pax_global_header00006660000000000000000000000064132343613460014517gustar00rootroot0000000000000052 comment=efcca963c478ea5fd7d9f56f619260480904c0f3 molequeue-0.9.0/000077500000000000000000000000001323436134600135265ustar00rootroot00000000000000molequeue-0.9.0/CMakeLists.txt000066400000000000000000000060101323436134600162630ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.3 FATAL_ERROR) project(MoleQueue) set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) # Request C++11 standard, using new CMake variables. set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED True) set(CMAKE_CXX_EXTENSIONS False) # Set symbol visibility defaults for all targets. set(CMAKE_CXX_VISIBILITY_PRESET "hidden") set(CMAKE_VISIBILITY_INLINES_HIDDEN True) include(BuildType) include(BuildLocation) include(CompilerFlags) include(InstallLocation) include(SilenceWarnings) include(DetermineVersion) # Set up our version. set(MoleQueue_VERSION_MAJOR "0") set(MoleQueue_VERSION_MINOR "9") set(MoleQueue_VERSION_PATCH "0") set(MoleQueue_VERSION "${MoleQueue_VERSION_MAJOR}.${MoleQueue_VERSION_MINOR}.${MoleQueue_VERSION_PATCH}") find_package(Git) determine_version(${MoleQueue_SOURCE_DIR} ${GIT_EXECUTABLE} "MoleQueue") if(APPLE) set(MACOSX_BUNDLE_NAME "MoleQueue") # Handle installation prefix for Mac bundles... option(USE_BUNDLE_LAYOUT "Use the Mac OS X bundle layout" OFF) endif() if(APPLE AND USE_BUNDLE_LAYOUT) set(prefix "${MACOSX_BUNDLE_NAME}.app/Contents") set(INSTALL_INCLUDE_DIR "${prefix}/${INSTALL_INCLUDE_DIR}") set(INSTALL_RUNTIME_DIR "${prefix}/MacOS") set(INSTALL_LIBRARY_DIR "${prefix}/${INSTALL_LIBRARY_DIR}") set(INSTALL_ARCHIVE_DIR "${prefix}/${INSTALL_ARCHIVE_DIR}") set(INSTALL_DATA_DIR "${prefix}/${INSTALL_DATA_DIR}") set(INSTALL_DOC_DIR "${prefix}/${INSTALL_DOC_DIR}") set(INSTALL_CMAKE_DIR "${prefix}/Resources") else() set(INSTALL_CMAKE_DIR "${INSTALL_LIBRARY_DIR}/cmake/molequeue") endif() option(ENABLE_TESTING "Enable testing" OFF) option(BUILD_SHARED_LIBS "Build with shared libraries" ON) option(MoleQueue_USE_EZHPC_UIT "Build support for ezHPC UIT interface" OFF) if(ENABLE_TESTING) include(CTest) enable_testing() endif() # This is necessary for tests, includes etc. include_directories(BEFORE "${MoleQueue_SOURCE_DIR}") set(EXPORT_TARGETS_NAME "MoleQueueTargets") add_subdirectory(molequeue) option(BUILD_DOCUMENTATION "Build project documentation" OFF) if(BUILD_DOCUMENTATION) add_subdirectory(docs) endif() if(USE_ZERO_MQ) add_subdirectory(python) endif() if(ENABLE_TESTING) include(BuildPackageTest) BuildPackageTest_Add("MoleQueue" "${CMAKE_CURRENT_BINARY_DIR}") endif() configure_file(${MoleQueue_SOURCE_DIR}/cmake/CTestCustom.cmake.in ${MoleQueue_BINARY_DIR}/CTestCustom.cmake) configure_file("${MoleQueue_SOURCE_DIR}/cmake/MoleQueueConfig.cmake.in" "${MoleQueue_BINARY_DIR}/MoleQueueConfig.cmake" @ONLY) configure_file("${MoleQueue_SOURCE_DIR}/cmake/MoleQueueConfigVersion.cmake.in" "${MoleQueue_BINARY_DIR}/MoleQueueConfigVersion.cmake" @ONLY) install(FILES "${MoleQueue_BINARY_DIR}/MoleQueueConfig.cmake" "${MoleQueue_BINARY_DIR}/MoleQueueConfigVersion.cmake" DESTINATION "${INSTALL_CMAKE_DIR}") install(EXPORT "MoleQueueTargets" DESTINATION "${INSTALL_CMAKE_DIR}") install( FILES README.md CONTRIBUTING.md LICENSE DESTINATION "${INSTALL_DOC_DIR}/molequeue") include(MoleQueueCPack) molequeue-0.9.0/CONTRIBUTING.md000066400000000000000000000015731323436134600157650ustar00rootroot00000000000000Contributing ------------ Our project uses the standard GitHub pull request process for code review and integration. Please check our [development][Development] guide for more details on developing and contributing to the project. The GitHub issue tracker can be used to report bugs, make feature requests, etc. Our [wiki][Wiki] is used to document features, flesh out designs and host other documentation. Our API is [documented using Doxygen][Doxygen] with updated documentation generated nightly. We have several [mailing lists][MailingLists] to coordinate development and to provide support. [Development]: http://wiki.openchemistry.org/Development "Development guide" [Wiki]: http://wiki.openchemistry.org/ "Open Chemistry wiki" [Doxygen]: http://doc.openchemistry.org/molequeue/api/ "API documentation" [MailingLists]: http://openchemistry.org/mailing-lists "Mailing Lists" molequeue-0.9.0/CTestConfig.cmake000066400000000000000000000003701323436134600167000ustar00rootroot00000000000000set(CTEST_PROJECT_NAME "MoleQueue") set(CTEST_NIGHTLY_START_TIME "01:00:00 UTC") set(CTEST_DROP_METHOD "http") set(CTEST_DROP_SITE "cdash.openchemistry.org") set(CTEST_DROP_LOCATION "/submit.php?project=MoleQueue") set(CTEST_DROP_SITE_CDASH TRUE) molequeue-0.9.0/LICENSE000066400000000000000000000027441323436134600145420ustar00rootroot00000000000000Copyright (c) 2011-2018, Kitware, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the MoleQueue project nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR 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 BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 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. molequeue-0.9.0/README.md000066400000000000000000000056541323436134600150170ustar00rootroot00000000000000MoleQueue ========= ![MoleQueue][MoleQueueLogo] Introduction ------------ MoleQueue is an open-source, cross-platform, system-tray resident desktop application for abstracting, managing, and coordinating the execution of tasks both locally and on remote computational resources. Users can set up local and remote queues that describe where the task will be executed. Each queue can have programs, with templates to facilitate the execution of the program. Input files can be staged, and output files collected using a standard interface. Some highlights: * Open source distributed under the liberal 3-clause BSD license * Cross platform with nightly builds on Linux, Mac OS X and Windows * Intuitive interface designed to be useful to whole community * Support for local executation and remote schedulers (SGE, PBS, SLURM) * System tray resident application managing queue of queues and job lifetime * Simple, lightweight JSON-RPC 2.0 based communication over local sockets * Qt 5 client library for simple integration in Qt applications ![Open Chemistry project][OpenChemistryLogo] ![Kitware, Inc.][KitwareLogo] MoleQueue is being developed as part of the [Open Chemistry][OpenChemistry] project at [Kitware][Kitware], along with companion tools and libraries to support the work. Installing ---------- We provide nightly binaries built by our [dashboards][Dashboard] for Mac OS X and Windows. If you would like to build from source we recommend that you follow our [building Open Chemistry][Build] guide that will take care of building most dependencies. Contributing ------------ Our project uses the standard GitHub pull request process for code review and integration. Please check our [development][Development] guide for more details on developing and contributing to the project. The GitHub issue tracker can be used to report bugs, make feature requests, etc. Our [wiki][Wiki] is used to document features, flesh out designs and host other documentation. Our API is [documented using Doxygen][Doxygen] with updated documentation generated nightly. We have several [mailing lists][MailingLists] to coordinate development and to provide support. [MoleQueueLogo]: http://openchemistry.org/files/logos/molequeue.png "MoleQueue" [OpenChemistry]: http://openchemistry.org/ "Open Chemistry Project" [OpenChemistryLogo]: http://openchemistry.org/files/logos/openchem128.png "Open Chemistry" [Kitware]: http://kitware.com/ "Kitware, Inc." [KitwareLogo]: http://www.kitware.com/img/small_logo_over.png "Kitware" [Dashboard]: http://cdash.openchemistry.org/index.php?project=MoleQueue "MoleQueue Dashboard" [Build]: http://wiki.openchemistry.org/Build "Building MoleQueue" [Development]: http://wiki.openchemistry.org/Development "Development guide" [Wiki]: http://wiki.openchemistry.org/ "Open Chemistry wiki" [Doxygen]: http://doc.openchemistry.org/molequeue/api/ "API documentation" [MailingLists]: http://openchemistry.org/mailing-lists "Mailing Lists" molequeue-0.9.0/cmake/000077500000000000000000000000001323436134600146065ustar00rootroot00000000000000molequeue-0.9.0/cmake/BuildLocation.cmake000066400000000000000000000006031323436134600203370ustar00rootroot00000000000000# Set up our directory structure for output libraries and binaries if(NOT CMAKE_RUNTIME_OUTPUT_DIRECTORY) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") endif() if(NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY) if(UNIX) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") else() set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") endif() endif() molequeue-0.9.0/cmake/BuildPackage.cmake.in000066400000000000000000000025211323436134600205300ustar00rootroot00000000000000# # Script used by the BuildPackageTest.cmake module. # message(STATUS "Running script '${CMAKE_CURRENT_LIST_FILE}'") message(STATUS " executing command '\"${CMAKE_COMMAND}\" --build . --config \"${config}\" --target package'") message(STATUS " in binary_dir='@binary_dir@'") message(STATUS "") set(results_script "@binary_dir@/BuildPackageTestResults.cmake") file(REMOVE "${results_script}") execute_process( COMMAND ${CMAKE_COMMAND} --build . --config "${config}" --target package WORKING_DIRECTORY "@binary_dir@" OUTPUT_VARIABLE output RESULT_VARIABLE result ) message(STATUS "output:") message(STATUS "${output}") message(STATUS "") if(NOT "${result}" STREQUAL "0") message(FATAL_ERROR "error: --build package call returned '${result}'") endif() # # Construct a list of package files by scraping individual file names from # the 'make package' output: # set(packages) set(regex "package: ([^\n]+) generated") string(REGEX MATCHALL "${regex}" package_lines "${output}") foreach(line ${package_lines}) string(REGEX REPLACE "${regex}" "\\1" package "${line}") list(APPEND packages "${package}") endforeach() # # Write out a helper script that can be used later to upload these: # file(WRITE "${results_script}" "# BuildPackageTestResults.cmake # generated by \"${CMAKE_CURRENT_LIST_FILE}\" # set(package_files \"${packages}\") ") molequeue-0.9.0/cmake/BuildPackageTest.cmake000066400000000000000000000020741323436134600207660ustar00rootroot00000000000000get_filename_component(_BuildPackageTest_self_dir "${CMAKE_CURRENT_LIST_FILE}" PATH) if("$ENV{DASHBOARD_TEST_FROM_CTEST}" STREQUAL "") # Not a dashboard, do not add BuildPackage* tests by default: set(BUILD_PACKAGE_TEST_DEFAULT OFF) else() # Dashboard, do add BuildPackage* tests by default: set(BUILD_PACKAGE_TEST_DEFAULT ON) endif() option(BUILD_PACKAGE_TEST "Add BuildPackage* tests..." ${BUILD_PACKAGE_TEST_DEFAULT}) function(BuildPackageTest_Add projname binary_dir) if (NOT BUILD_PACKAGE_TEST) return() endif() # Use the NAME/COMMAND form of add_test and pass $. # However, using this form requires passing -C when running ctest # from the command line, or setting CTEST_CONFIGURATION_TYPE # in a -S script. configure_file( ${_BuildPackageTest_self_dir}/BuildPackage.cmake.in ${binary_dir}/BuildPackage${projname}.cmake @ONLY ) add_test( NAME BuildPackage${projname} COMMAND ${CMAKE_COMMAND} -D config=$ -P ${binary_dir}/BuildPackage${projname}.cmake ) endfunction() molequeue-0.9.0/cmake/BuildType.cmake000066400000000000000000000006561323436134600175200ustar00rootroot00000000000000# Set a default build type if none was specified if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) message(STATUS "Setting build type to 'Debug' as none was specified.") set(CMAKE_BUILD_TYPE Debug CACHE STRING "Choose the type of build." FORCE) # Set the possible values of build type for cmake-gui set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") endif() molequeue-0.9.0/cmake/CTestCustom.cmake.in000066400000000000000000000006331323436134600204340ustar00rootroot00000000000000set(CTEST_CUSTOM_COVERAGE_EXCLUDE ${CTEST_CUSTOM_COVERAGE_EXCLUDE} "/molequeue/testing/" "/moc_" "/ui_" "/thirdparty/" ) set(CTEST_CUSTOM_WARNING_EXCEPTION ${CTEST_CUSTOM_WARNING_EXCEPTION} # We don't care about warnings from third party libraries: ".*thirdparty.*" # Nested Qt foreach loops produce this warning: "_container_.* shadows a previous local" "shadowed declaration is here" ) molequeue-0.9.0/cmake/CompilerFlags.cmake000066400000000000000000000025301323436134600203370ustar00rootroot00000000000000if(CMAKE_COMPILER_IS_GNUCXX) include(CheckCXXCompilerFlag) # Addtional warnings for GCC set(CMAKE_CXX_FLAGS_WARN "-Wnon-virtual-dtor -Wno-long-long -ansi -Wcast-align -Wchar-subscripts -Wall -Wextra -Wpointer-arith -Wformat-security -Woverloaded-virtual -Wshadow -Wunused-parameter -fno-check-new -fno-common") # This flag is useful as not returning from a non-void function is an error # with MSVC, but it is not supported on all GCC compiler versions check_cxx_compiler_flag(-Werror=return-type HAVE_GCC_ERROR_RETURN_TYPE) if(HAVE_GCC_ERROR_RETURN_TYPE) set(CMAKE_CXX_FLAGS_ERROR "-Werror=return-type") endif() # If we are compiling on Linux then set some extra linker flags too if(CMAKE_SYSTEM_NAME MATCHES Linux) set(CMAKE_SHARED_LINKER_FLAGS "-Wl,--fatal-warnings -Wl,--no-undefined -lc ${CMAKE_SHARED_LINKER_FLAGS}") set(CMAKE_MODULE_LINKER_FLAGS "-Wl,--fatal-warnings -Wl,--no-undefined -lc ${CMAKE_MODULE_LINKER_FLAGS}") set (CMAKE_EXE_LINKER_FLAGS "-Wl,--fatal-warnings -Wl,--no-undefined -lc ${CMAKE_EXE_LINKER_FLAGS}") endif() # Set up the debug CXX_FLAGS for extra warnings set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} ${CMAKE_CXX_FLAGS_WARN}") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${CMAKE_CXX_FLAGS_WARN} ${CMAKE_CXX_FLAGS_ERROR}") endif() molequeue-0.9.0/cmake/DeployQt5.cmake000066400000000000000000000302351323436134600174410ustar00rootroot00000000000000#.rst: # DeployQt5 # --------- # # Functions to help assemble a standalone Qt5 executable. # # A collection of CMake utility functions useful for deploying Qt5 # executables. # # The following functions are provided by this module: # # :: # # write_qt5_conf # resolve_qt5_paths # fixup_qt5_executable # install_qt5_plugin_path # install_qt5_plugin # install_qt5_executable # # Requires CMake 2.8.9 or greater because Qt 5 does. # Also depends on BundleUtilities.cmake. # # :: # # WRITE_QT5_CONF( ) # # Writes a qt.conf file with the into . # # :: # # RESOLVE_QT5_PATHS( []) # # Loop through list and if any don't exist resolve them # relative to the (if supplied) or the # CMAKE_INSTALL_PREFIX. # # :: # # FIXUP_QT5_EXECUTABLE( [ ]) # # Copies Qt plugins, writes a Qt configuration file (if needed) and # fixes up a Qt5 executable using BundleUtilities so it is standalone # and can be drag-and-drop copied to another machine as long as all of # the system libraries are compatible. # # should point to the executable to be fixed-up. # # should contain a list of the names or paths of any Qt # plugins to be installed. # # will be passed to BundleUtilities and should be a list of any # already installed plugins, libraries or executables to also be # fixed-up. # # will be passed to BundleUtilities and should contain and # directories to be searched to find library dependencies. # # allows an custom plugins directory to be used. # # will force a qt.conf file to be written even if not # needed. # # :: # # INSTALL_QT5_PLUGIN_PATH(plugin executable copy installed_plugin_path_var ) # # Install (or copy) a resolved to the default plugins directory # (or ) relative to and store the result in # . # # If is set to TRUE then the plugins will be copied rather than # installed. This is to allow this module to be used at CMake time # rather than install time. # # If is set then anything installed will use this COMPONENT. # # :: # # INSTALL_QT5_PLUGIN(plugin executable copy installed_plugin_path_var ) # # Install (or copy) an unresolved to the default plugins # directory (or ) relative to and store the # result in . See documentation of # INSTALL_QT5_PLUGIN_PATH. # # :: # # INSTALL_QT5_EXECUTABLE( [ ]) # # Installs Qt plugins, writes a Qt configuration file (if needed) and # fixes up a Qt5 executable using BundleUtilities so it is standalone # and can be drag-and-drop copied to another machine as long as all of # the system libraries are compatible. The executable will be fixed-up # at install time. is the COMPONENT used for bundle fixup # and plugin installation. See documentation of FIXUP_QT5_BUNDLE. #============================================================================= # Copyright 2011 Mike McQuaid # # Distributed under the OSI-approved BSD License (the "License"); # see accompanying file Copyright.txt for details. # # This software is distributed WITHOUT ANY WARRANTY; without even the # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the License for more information. #============================================================================= # (To distribute this file outside of CMake, substitute the full # License text for the above reference.) # The functions defined in this file depend on the fixup_bundle function # (and others) found in BundleUtilities.cmake include(BundleUtilities) set(DeployQt5_cmake_dir "${CMAKE_CURRENT_LIST_DIR}") set(DeployQt5_apple_plugins_dir "PlugIns") function(write_qt5_conf qt_conf_dir qt_conf_contents) set(qt_conf_path "${qt_conf_dir}/qt.conf") message(STATUS "Writing ${qt_conf_path}") file(WRITE "${qt_conf_path}" "${qt_conf_contents}") endfunction() function(resolve_qt5_paths paths_var) set(executable_path ${ARGV1}) set(paths_resolved) foreach(path ${${paths_var}}) if(EXISTS "${path}") list(APPEND paths_resolved "${path}") else() if(${executable_path}) list(APPEND paths_resolved "${executable_path}/${path}") else() list(APPEND paths_resolved "\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${path}") endif() endif() endforeach() set(${paths_var} ${paths_resolved} PARENT_SCOPE) endfunction() function(fixup_qt5_executable executable) set(qtplugins ${ARGV1}) set(libs ${ARGV2}) set(dirs ${ARGV3}) set(plugins_dir ${ARGV4}) set(request_qt_conf ${ARGV5}) message(STATUS "fixup_qt5_executable") message(STATUS " executable='${executable}'") message(STATUS " qtplugins='${qtplugins}'") message(STATUS " libs='${libs}'") message(STATUS " dirs='${dirs}'") message(STATUS " plugins_dir='${plugins_dir}'") message(STATUS " request_qt_conf='${request_qt_conf}'") if(QT_LIBRARY_DIR) list(APPEND dirs "${QT_LIBRARY_DIR}") endif() if(QT_BINARY_DIR) list(APPEND dirs "${QT_BINARY_DIR}") endif() if(APPLE) set(qt_conf_dir "${executable}/Contents/Resources") set(executable_path "${executable}") set(write_qt_conf TRUE) if(NOT plugins_dir) set(plugins_dir "${DeployQt5_apple_plugins_dir}") endif() else() get_filename_component(executable_path "${executable}" PATH) if(NOT executable_path) set(executable_path ".") endif() set(qt_conf_dir "${executable_path}") set(write_qt_conf ${request_qt_conf}) endif() foreach(plugin ${qtplugins}) set(installed_plugin_path "") install_qt5_plugin("${plugin}" "${executable}" 1 installed_plugin_path) list(APPEND libs ${installed_plugin_path}) endforeach() foreach(lib ${libs}) if(NOT EXISTS "${lib}") message(FATAL_ERROR "Library does not exist: ${lib}") endif() endforeach() resolve_qt5_paths(libs "${executable_path}") if(write_qt_conf) set(qt_conf_contents "[Paths]\nPlugins = ${plugins_dir}") write_qt5_conf("${qt_conf_dir}" "${qt_conf_contents}") endif() fixup_bundle("${executable}" "${libs}" "${dirs}") endfunction() function(install_qt5_plugin_path plugin executable copy installed_plugin_path_var) set(plugins_dir ${ARGV4}) set(component ${ARGV5}) set(configurations ${ARGV6}) if(EXISTS "${plugin}") if(APPLE) if(NOT plugins_dir) set(plugins_dir "${DeployQt5_apple_plugins_dir}") endif() set(plugins_path "${executable}/Contents/${plugins_dir}") else() get_filename_component(plugins_path "${executable}" PATH) if(NOT plugins_path) set(plugins_path ".") endif() if(plugins_dir) set(plugins_path "${plugins_path}/${plugins_dir}") endif() endif() set(plugin_group "") get_filename_component(plugin_path "${plugin}" PATH) get_filename_component(plugin_parent_path "${plugin_path}" PATH) get_filename_component(plugin_parent_dir_name "${plugin_parent_path}" NAME) get_filename_component(plugin_name "${plugin}" NAME) string(TOLOWER "${plugin_parent_dir_name}" plugin_parent_dir_name) if("${plugin_parent_dir_name}" STREQUAL "plugins") get_filename_component(plugin_group "${plugin_path}" NAME) set(${plugin_group_var} "${plugin_group}") endif() set(plugins_path "${plugins_path}/${plugin_group}") if(${copy}) file(MAKE_DIRECTORY "${plugins_path}") file(COPY "${plugin}" DESTINATION "${plugins_path}") else() if(configurations AND (CMAKE_CONFIGURATION_TYPES OR CMAKE_BUILD_TYPE)) set(configurations CONFIGURATIONS ${configurations}) else() unset(configurations) endif() install(FILES "${plugin}" DESTINATION "${plugins_path}" ${configurations} ${component}) endif() set(${installed_plugin_path_var} "${plugins_path}/${plugin_name}" PARENT_SCOPE) endif() endfunction() function(install_qt5_plugin plugin executable copy installed_plugin_path_var) set(plugins_dir ${ARGV4}) set(component ${ARGV5}) if(EXISTS "${plugin}") install_qt5_plugin_path("${plugin}" "${executable}" "${copy}" "${installed_plugin_path_var}" "${plugins_dir}" "${component}") else() string(TOUPPER "QT_${plugin}_PLUGIN" plugin_var) set(plugin_release_var "${plugin_var}_RELEASE") set(plugin_debug_var "${plugin_var}_DEBUG") set(plugin_release "${${plugin_release_var}}") set(plugin_debug "${${plugin_debug_var}}") if(DEFINED "${plugin_release_var}" AND DEFINED "${plugin_debug_var}" AND NOT EXISTS "${plugin_release}" AND NOT EXISTS "${plugin_debug}") message(WARNING "Qt plugin \"${plugin}\" not recognized or found.") endif() if(NOT EXISTS "${${plugin_debug_var}}") set(plugin_debug "${plugin_release}") endif() if(CMAKE_CONFIGURATION_TYPES OR CMAKE_BUILD_TYPE) install_qt5_plugin_path("${plugin_release}" "${executable}" "${copy}" "${installed_plugin_path_var}_release" "${plugins_dir}" "${component}" "Release|RelWithDebInfo|MinSizeRel") install_qt5_plugin_path("${plugin_debug}" "${executable}" "${copy}" "${installed_plugin_path_var}_debug" "${plugins_dir}" "${component}" "Debug") if(CMAKE_BUILD_TYPE MATCHES "^Debug$") set(${installed_plugin_path_var} ${${installed_plugin_path_var}_debug}) else() set(${installed_plugin_path_var} ${${installed_plugin_path_var}_release}) endif() else() install_qt5_plugin_path("${plugin_release}" "${executable}" "${copy}" "${installed_plugin_path_var}" "${plugins_dir}" "${component}") endif() endif() set(${installed_plugin_path_var} ${${installed_plugin_path_var}} PARENT_SCOPE) endfunction() function(install_qt5_executable executable) set(qtplugins ${ARGV1}) set(libs ${ARGV2}) set(dirs ${ARGV3}) set(plugins_dir ${ARGV4}) set(request_qt_conf ${ARGV5}) set(component ${ARGV6}) if(QT_LIBRARY_DIR) list(APPEND dirs "${QT_LIBRARY_DIR}") endif() if(QT_BINARY_DIR) list(APPEND dirs "${QT_BINARY_DIR}") endif() if(TARGET Qt5::Core) get_property(_locCore TARGET Qt5::Core PROPERTY LOCATION_RELEASE) get_filename_component(_loc ${_locCore} DIRECTORY) message(STATUS "Adding Qt 5 directory: ${_loc}") list(APPEND dirs "${_loc}") else() message(FATAL_ERROR "No Qt5::Core target found, ensure it is available") endif() if(component) set(component COMPONENT ${component}) else() unset(component) endif() get_filename_component(executable_absolute "${executable}" ABSOLUTE) if(EXISTS "${QT_QTCORE_LIBRARY_RELEASE}") gp_file_type("${executable_absolute}" "${QT_QTCORE_LIBRARY_RELEASE}" qtcore_type) elseif(EXISTS "${QT_QTCORE_LIBRARY_DEBUG}") gp_file_type("${executable_absolute}" "${QT_QTCORE_LIBRARY_DEBUG}" qtcore_type) endif() if(qtcore_type STREQUAL "system") set(qt_plugins_dir "") endif() if(QT_IS_STATIC) message(WARNING "Qt built statically: not installing plugins.") else() if(APPLE) get_property(loc TARGET Qt5::QCocoaIntegrationPlugin PROPERTY LOCATION_RELEASE) install_qt5_plugin("${loc}" "${executable}" 0 installed_plugin_paths "PlugIns" "${component}") list(APPEND libs ${installed_plugin_paths}) elseif(WIN32) get_property(loc TARGET Qt5::QWindowsIntegrationPlugin PROPERTY LOCATION_RELEASE) install_qt5_plugin("${loc}" "${executable}" 0 installed_plugin_paths "" "${component}") list(APPEND libs ${installed_plugin_paths}) endif() foreach(plugin ${qtplugins}) set(installed_plugin_paths "") install_qt5_plugin("${plugin}" "${executable}" 0 installed_plugin_paths "${plugins_dir}" "${component}") list(APPEND libs ${installed_plugin_paths}) endforeach() endif() resolve_qt5_paths(libs "") install(CODE "include(\"${DeployQt5_cmake_dir}/DeployQt5.cmake\") set(BU_CHMOD_BUNDLE_ITEMS TRUE) fixup_qt5_executable(\"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${executable}\" \"\" \"${libs}\" \"${dirs}\" \"${plugins_dir}\" \"${request_qt_conf}\")" ${component} ) endfunction() molequeue-0.9.0/cmake/DetermineVersion.cmake000066400000000000000000000042331323436134600210740ustar00rootroot00000000000000# Used to determine the version for OpenChemistry source using "git describe", if git # is found. On success sets following variables in caller's scope: # ${var_prefix}_VERSION # ${var_prefix}_VERSION_MAJOR # ${var_prefix}_VERSION_MINOR # ${var_prefix}_VERSION_PATCH # ${var_prefix}_VERSION_PATCH_EXTRA # ${var_prefix}_VERSION_IS_RELEASE is patch-extra is empty. # # If git is not found, or git describe cannot be run successfully, then these # variables are left unchanged and status message is printed. # # Arguments are: # source_dir : Source directory # git_command : git executable # var_prefix : prefix for variables e.g. "AvogadroApp". function(determine_version source_dir git_command var_prefix) set (major) set (minor) set (patch) set (full) set (patch_extra) if (EXISTS ${git_command}) execute_process( COMMAND ${git_command} describe WORKING_DIRECTORY ${source_dir} RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_STRIP_TRAILING_WHITESPACE) if (${result} EQUAL 0) string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)[-]*(.*)" version_matches ${output}) if (CMAKE_MATCH_0) message(STATUS "Determined Source Version : ${CMAKE_MATCH_0}") set (full ${CMAKE_MATCH_0}) set (major ${CMAKE_MATCH_1}) set (minor ${CMAKE_MATCH_2}) set (patch ${CMAKE_MATCH_3}) set (patch_extra ${CMAKE_MATCH_4}) endif() endif() endif() if (full) set (${var_prefix}_VERSION ${full} PARENT_SCOPE) set (${var_prefix}_VERSION_MAJOR ${major} PARENT_SCOPE) set (${var_prefix}_VERSION_MINOR ${minor} PARENT_SCOPE) set (${var_prefix}_VERSION_PATCH ${patch} PARENT_SCOPE) set (${var_prefix}_VERSION_PATCH_EXTRA ${patch_extra} PARENT_SCOPE) if ("${major}.${minor}.${patch}" EQUAL "${full}") set (${var_prefix}_VERSION_IS_RELEASE TRUE PARENT_SCOPE) else () set (${var_prefix}_VERSION_IS_RELEASE FALSE PARENT_SCOPE) endif() else() message(STATUS "Could not use git to determine source version, using version ${${var_prefix}_VERSION}" ) endif() endfunction()molequeue-0.9.0/cmake/FindZeroMQ.cmake000066400000000000000000000021461323436134600175710ustar00rootroot00000000000000# - Try to find ZeroMQ headers and libraries # - THANKS CUBIT FOR THIS FIND MODULE # # Usage of this module as follows: # # find_package(ZeroMQ) # # Variables used by this module, they can change the default behaviour and need # to be set before calling find_package: # # ZeroMQ_ROOT_DIR Set this variable to the root installation of # ZeroMQ if the module has problems finding # the proper installation path. # # Variables defined by this module: # # ZEROMQ_FOUND System has ZeroMQ libs/headers # ZeroMQ_LIBRARIES The ZeroMQ libraries # ZeroMQ_INCLUDE_DIR The location of ZeroMQ headers find_path(ZeroMQ_ROOT_DIR NAMES include/zmq.hpp ) find_library(ZeroMQ_LIBRARIES NAMES zmq HINTS ${ZeroMQ_ROOT_DIR}/lib ) find_path(ZeroMQ_INCLUDE_DIR NAMES zmq.hpp HINTS ${ZeroMQ_ROOT_DIR}/include ) include(FindPackageHandleStandardArgs) find_package_handle_standard_args(ZeroMQ DEFAULT_MSG ZeroMQ_LIBRARIES ZeroMQ_INCLUDE_DIR ) mark_as_advanced( ZeroMQ_ROOT_DIR ZeroMQ_LIBRARIES ZeroMQ_INCLUDE_DIR ) molequeue-0.9.0/cmake/InstallLocation.cmake000066400000000000000000000032561323436134600207150ustar00rootroot00000000000000# Some default installation locations. These should be global, with any project # specific locations added to the end. These paths are all relative to the # install prefix. # # These paths attempt to adhere to the FHS, and are similar to those provided # by autotools and used in many Linux distributions. # # Use GNU install directories include(GNUInstallDirs) if(NOT INSTALL_RUNTIME_DIR) set(INSTALL_RUNTIME_DIR "${CMAKE_INSTALL_BINDIR}") endif() if(NOT INSTALL_LIBRARY_DIR) set(INSTALL_LIBRARY_DIR "${CMAKE_INSTALL_LIBDIR}") endif() if(NOT INSTALL_ARCHIVE_DIR) set(INSTALL_ARCHIVE_DIR "${CMAKE_INSTALL_LIBDIR}") endif() if(NOT INSTALL_INCLUDE_DIR) set(INSTALL_INCLUDE_DIR "${CMAKE_INSTALL_INCLUDEDIR}") endif() if(NOT INSTALL_DATA_DIR) set(INSTALL_DATA_DIR "${CMAKE_INSTALL_DATAROOTDIR}") endif() if(NOT INSTALL_DOC_DIR) set(INSTALL_DOC_DIR "${CMAKE_INSTALL_DOCDIR}") endif() if(NOT INSTALL_MAN_DIR) set(INSTALL_MAN_DIR "${CMAKE_INSTALL_MANDIR}") endif() if(UNIX AND NOT APPLE) if(NOT INSTALL_XDG_APP_DIR) set(INSTALL_XDG_APPS_DIR "${INSTALL_DATA_DIR}/applications") endif() if(NOT INSTALL_XDG_ICON_DIR) set(INSTALL_XDG_ICON_DIR "${INSTALL_DATA_DIR}/pixmaps") endif() endif() # Set up RPATH for the project too. option(ENABLE_RPATH "Enable rpath support on Linux and Mac" ON) if(NOT CMAKE_INSTALL_RPATH) set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${INSTALL_LIBRARY_DIR}") endif() if(APPLE AND NOT CMAKE_INSTALL_NAME_DIR) set(CMAKE_INSTALL_NAME_DIR "${CMAKE_INSTALL_PREFIX}/${INSTALL_LIBRARY_DIR}") endif() if(UNIX AND ENABLE_RPATH) set(CMAKE_SKIP_BUILD_RPATH FALSE) set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE) set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) endif() molequeue-0.9.0/cmake/MoleQueueCPack.cmake000066400000000000000000000022741323436134600204200ustar00rootroot00000000000000set(CPACK_PACKAGE_NAME "MoleQueue") set(CPACK_PACKAGE_VERSION_MAJOR ${MoleQueue_VERSION_MAJOR}) set(CPACK_PACKAGE_VERSION_MINOR ${MoleQueue_VERSION_MINOR}) set(CPACK_PACKAGE_VERSION_PATCH ${MoleQueue_VERSION_PATCH}) set(CPACK_PACKAGE_VERSION ${MoleQueue_VERSION}) set(CPACK_PACKAGE_INSTALL_DIRECTORY "MoleQueue") set(CPACK_PACKAGE_VENDOR "http://openchemistry.org/") set(CPACK_PACKAGE_DESCRIPTION "Desktop integration of high performance computing resources.") if(APPLE) configure_file("${MoleQueue_SOURCE_DIR}/LICENSE" "${MoleQueue_BINARY_DIR}/LICENSE.txt" @ONLY) set(CPACK_RESOURCE_FILE_LICENSE "${MoleQueue_BINARY_DIR}/LICENSE.txt") set(CPACK_PACKAGE_ICON "${MoleQueue_SOURCE_DIR}/molequeue/app/icons/molequeue.icns") set(CPACK_BUNDLE_ICON "${CPACK_PACKAGE_ICON}") else() set(CPACK_RESOURCE_FILE_LICENSE "${MoleQueue_SOURCE_DIR}/LICENSE") endif() set(CPACK_PACKAGE_EXECUTABLES "molequeue" "MoleQueue") set(CPACK_CREATE_DESKTOP_LINKS "molequeue") configure_file("${CMAKE_CURRENT_LIST_DIR}/MoleQueueCPackOptions.cmake.in" "${MoleQueue_BINARY_DIR}/MoleQueueCPackOptions.cmake" @ONLY) set(CPACK_PROJECT_CONFIG_FILE "${MoleQueue_BINARY_DIR}/MoleQueueCPackOptions.cmake") include(CPack) molequeue-0.9.0/cmake/MoleQueueCPackOptions.cmake.in000066400000000000000000000015771323436134600224060ustar00rootroot00000000000000# This file is configured at cmake time, loaded at cpack time. # NSIS specific settings if(CPACK_GENERATOR MATCHES "NSIS") set(CPACK_NSIS_MUI_ICON "@CMAKE_SOURCE_DIR@/molequeue/app/icons\\\\molequeue.ico") set(CPACK_NSIS_HELP_LINK "http:\\\\openchemistry.org") set(CPACK_NSIS_URL_INFO_ABOUT "http:\\\\openchemistry.org") set(CPACK_PACKAGE_EXECUTABLES "molequeue" "MoleQueue") set(CPACK_CREATE_DESKTOP_LINKS "molequeue") set(CPACK_NSIS_INSTALLED_ICON_NAME "bin\\\\MoleQueue.exe") set(CPACK_NSIS_MENU_LINKS "http://wiki.openchemistry.org/MoleQueue_@CPACK_PACKAGE_VERSION@" "Release Notes" "http://openchemistry.org/" "Open Chemistry Project") set(CPACK_NSIS_MODIFY_PATH ON) endif(CPACK_GENERATOR MATCHES "NSIS") if("${CPACK_GENERATOR}" STREQUAL "PackageMaker") set(CPACK_PACKAGE_DEFAULT_LOCATION "/Applications") endif("${CPACK_GENERATOR}" STREQUAL "PackageMaker") molequeue-0.9.0/cmake/MoleQueueConfig.cmake.in000066400000000000000000000016301323436134600212440ustar00rootroot00000000000000# MoleQueue CMake configuration file - http://www.openchemistry.org/ # If this file was found, then MoleQueue has been found set(MoleQueue_FOUND 1) set(MoleQueue_VERSION_MAJOR "@MoleQueue_VERSION_MAJOR@") set(MoleQueue_VERSION_MINOR "@MoleQueue_VERSION_MINOR@") set(MoleQueue_VERSION_PATCH "@MoleQueue_VERSION_PATCH@") set(MoleQueue_VERSION "${MoleQueue_VERSION_MAJOR}.${MoleQueue_VERSION_MINOR}.${MoleQueue_VERSION_PATCH}") set(MoleQueue_INSTALL_PREFIX "@CMAKE_INSTALL_PREFIX@") set(MoleQueue_INCLUDE_DIRS "${MoleQueue_INSTALL_PREFIX}/@INSTALL_INCLUDE_DIR@") set(MoleQueue_LIBRARY_DIR "${MoleQueue_INSTALL_PREFIX}/@INSTALL_LIBRARY_DIR@") set(MoleQueue_RUNTIME_DIR "${MoleQueue_INSTALL_PREFIX}/@INSTALL_RUNTIME_DIR@") set(MoleQueue_CMAKE_DIR "${MoleQueue_LIBRARY_DIR}/cmake/molequeue") if(NOT TARGET MoleQueueClient) include("${MoleQueue_CMAKE_DIR}/MoleQueueTargets.cmake") endif() molequeue-0.9.0/cmake/MoleQueueConfigVersion.cmake.in000066400000000000000000000011331323436134600226100ustar00rootroot00000000000000# MoleQueue CMake version file - http://www.openchemistry.org/ set(PACKAGE_VERSION @MoleQueue_VERSION_MAJOR@.@MoleQueue_VERSION_MINOR@.@MoleQueue_VERSION_PATCH@) if("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}") set(PACKAGE_VERSION_COMPATIBLE FALSE) else("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}") set(PACKAGE_VERSION_COMPATIBLE TRUE) if("${PACKAGE_FIND_VERSION}" STREQUAL "${PACKAGE_VERSION}") set(PACKAGE_VERSION_EXACT TRUE) endif("${PACKAGE_FIND_VERSION}" STREQUAL "${PACKAGE_VERSION}") endif("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}") molequeue-0.9.0/cmake/SilenceWarnings.cmake000066400000000000000000000007731323436134600207120ustar00rootroot00000000000000# This file provides a macro to disable compiler warnings on source files. # # usage: silence_warnings(${source_files}) macro(silence_warnings _sources) if(BORLAND) set_property(SOURCE ${_sources} PROPERTY COMPILE_FLAGS "-w-") else() set_property(SOURCE ${_sources} PROPERTY COMPILE_FLAGS "-w") endif() # Some specific MSVC warnings: if(MSVC) set_property(SOURCE ${_sources} PROPERTY COMPILE_FLAGS "-wd4068" # Unknown pragma ) endif() endmacro() molequeue-0.9.0/docs/000077500000000000000000000000001323436134600144565ustar00rootroot00000000000000molequeue-0.9.0/docs/CMakeLists.txt000066400000000000000000000007151323436134600172210ustar00rootroot00000000000000find_package(Doxygen REQUIRED) set(doxygen_source_dirs "${MoleQueue_SOURCE_DIR}/molequeue ${MoleQueue_SOURCE_DIR}/docs") set(doxygen_output_dir "${CMAKE_CURRENT_BINARY_DIR}") configure_file("${CMAKE_CURRENT_SOURCE_DIR}/doxyfile.in" "${CMAKE_CURRENT_BINARY_DIR}/doxyfile") add_custom_target(documentation COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_CURRENT_BINARY_DIR}/html COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/doxyfile) molequeue-0.9.0/docs/doxyfile.in000066400000000000000000002046641323436134600166450ustar00rootroot00000000000000# Doxyfile 1.7.1 # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project # # All text after a hash (#) is considered a comment and will be ignored # The format is: # TAG = value [value, ...] # For lists items can also be appended using: # TAG += value [value, ...] # Values that contain spaces should be placed between quotes (" ") #--------------------------------------------------------------------------- # Project related configuration options #--------------------------------------------------------------------------- # This tag specifies the encoding used for all characters in the config file # that follow. The default is UTF-8 which is also the encoding used for all # text before the first occurrence of this tag. Doxygen uses libiconv (or the # iconv built into libc) for the transcoding. See # http://www.gnu.org/software/libiconv for the list of possible encodings. DOXYFILE_ENCODING = UTF-8 # The PROJECT_NAME tag is a single word (or a sequence of words surrounded # by quotes) that should identify the project. PROJECT_NAME = @CMAKE_PROJECT_NAME@ # The PROJECT_NUMBER tag can be used to enter a project or revision number. # This could be handy for archiving the generated documentation or # if some version control system is used. PROJECT_NUMBER = @MoleQueue_VERSION@ # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) # base path where the generated documentation will be put. # If a relative path is entered, it will be relative to the location # where doxygen was started. If left blank the current directory will be used. OUTPUT_DIRECTORY = @doxygen_output_dir@ # If the CREATE_SUBDIRS tag is set to YES, then doxygen will create # 4096 sub-directories (in 2 levels) under the output directory of each output # format and will distribute the generated files over these directories. # Enabling this option can be useful when feeding doxygen a huge amount of # source files, where putting all generated files in the same directory would # otherwise cause performance problems for the file system. CREATE_SUBDIRS = NO # The OUTPUT_LANGUAGE tag is used to specify the language in which all # documentation generated by doxygen is written. Doxygen will use this # information to generate all constant output in the proper language. # The default language is English, other supported languages are: # Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional, # Croatian, Czech, Danish, Dutch, Esperanto, Farsi, Finnish, French, German, # Greek, Hungarian, Italian, Japanese, Japanese-en (Japanese with English # messages), Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian, # Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrilic, Slovak, # Slovene, Spanish, Swedish, Ukrainian, and Vietnamese. OUTPUT_LANGUAGE = English # If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will # include brief member descriptions after the members that are listed in # the file and class documentation (similar to JavaDoc). # Set to NO to disable this. BRIEF_MEMBER_DESC = NO # If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend # the brief description of a member or function before the detailed description. # Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the # brief descriptions will be completely suppressed. REPEAT_BRIEF = YES # This tag implements a quasi-intelligent brief description abbreviator # that is used to form the text in various listings. Each string # in this list, if found as the leading text of the brief description, will be # stripped from the text and the result after processing the whole list, is # used as the annotated text. Otherwise, the brief description is used as-is. # If left blank, the following values are used ("$name" is automatically # replaced with the name of the entity): "The $name class" "The $name widget" # "The $name file" "is" "provides" "specifies" "contains" # "represents" "a" "an" "the" ABBREVIATE_BRIEF = # If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then # Doxygen will generate a detailed section even if there is only a brief # description. ALWAYS_DETAILED_SEC = YES # If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all # inherited members of a class in the documentation of that class as if those # members were ordinary class members. Constructors, destructors and assignment # operators of the base classes will not be shown. INLINE_INHERITED_MEMB = NO # If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full # path before files name in the file list and in the header files. If set # to NO the shortest path that makes the file name unique will be used. FULL_PATH_NAMES = NO # If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag # can be used to strip a user-defined part of the path. Stripping is # only done if one of the specified strings matches the left-hand part of # the path. The tag can be used to show relative paths in the file list. # If left blank the directory from which doxygen is run is used as the # path to strip. STRIP_FROM_PATH = # The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of # the path mentioned in the documentation of a class, which tells # the reader which header file to include in order to use a class. # If left blank only the name of the header file containing the class # definition is used. Otherwise one should specify the include paths that # are normally passed to the compiler using the -I flag. STRIP_FROM_INC_PATH = # If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter # (but less readable) file names. This can be useful is your file systems # doesn't support long names like on DOS, Mac, or CD-ROM. SHORT_NAMES = NO # If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen # will interpret the first line (until the first dot) of a JavaDoc-style # comment as the brief description. If set to NO, the JavaDoc # comments will behave just like regular Qt-style comments # (thus requiring an explicit @brief command for a brief description.) JAVADOC_AUTOBRIEF = NO # If the QT_AUTOBRIEF tag is set to YES then Doxygen will # interpret the first line (until the first dot) of a Qt-style # comment as the brief description. If set to NO, the comments # will behave just like regular Qt-style comments (thus requiring # an explicit \brief command for a brief description.) QT_AUTOBRIEF = NO # The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen # treat a multi-line C++ special comment block (i.e. a block of //! or /// # comments) as a brief description. This used to be the default behaviour. # The new default is to treat a multi-line C++ comment block as a detailed # description. Set this tag to YES if you prefer the old behaviour instead. MULTILINE_CPP_IS_BRIEF = NO # If the INHERIT_DOCS tag is set to YES (the default) then an undocumented # member inherits the documentation from any documented member that it # re-implements. INHERIT_DOCS = YES # If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce # a new page for each member. If set to NO, the documentation of a member will # be part of the file/class/namespace that contains it. SEPARATE_MEMBER_PAGES = NO # The TAB_SIZE tag can be used to set the number of spaces in a tab. # Doxygen uses this value to replace tabs by spaces in code fragments. TAB_SIZE = 2 # This tag can be used to specify a number of aliases that acts # as commands in the documentation. An alias has the form "name=value". # For example adding "sideeffect=\par Side Effects:\n" will allow you to # put the command \sideeffect (or @sideeffect) in the documentation, which # will result in a user-defined paragraph with heading "Side Effects:". # You can put \n's in the value part of an alias to insert newlines. ALIASES = # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C # sources only. Doxygen will then generate output that is more tailored for C. # For instance, some of the names that are used will be different. The list # of all members will be omitted, etc. OPTIMIZE_OUTPUT_FOR_C = NO # Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java # sources only. Doxygen will then generate output that is more tailored for # Java. For instance, namespaces will be presented as packages, qualified # scopes will look different, etc. OPTIMIZE_OUTPUT_JAVA = NO # Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran # sources only. Doxygen will then generate output that is more tailored for # Fortran. OPTIMIZE_FOR_FORTRAN = NO # Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL # sources. Doxygen will then generate output that is tailored for # VHDL. OPTIMIZE_OUTPUT_VHDL = NO # Doxygen selects the parser to use depending on the extension of the files it # parses. With this tag you can assign which parser to use for a given extension. # Doxygen has a built-in mapping, but you can override or extend it using this # tag. The format is ext=language, where ext is a file extension, and language # is one of the parsers supported by doxygen: IDL, Java, Javascript, CSharp, C, # C++, D, PHP, Objective-C, Python, Fortran, VHDL, C, C++. For instance to make # doxygen treat .inc files as Fortran files (default is PHP), and .f files as C # (default is Fortran), use: inc=Fortran f=C. Note that for custom extensions # you also need to set FILE_PATTERNS otherwise the files are not read by doxygen. EXTENSION_MAPPING = # If you use STL classes (i.e. std::string, std::vector, etc.) but do not want # to include (a tag file for) the STL sources as input, then you should # set this tag to YES in order to let doxygen match functions declarations and # definitions whose arguments contain STL classes (e.g. func(std::string); v.s. # func(std::string) {}). This also make the inheritance and collaboration # diagrams that involve STL classes more complete and accurate. BUILTIN_STL_SUPPORT = YES # If you use Microsoft's C++/CLI language, you should set this option to YES to # enable parsing support. CPP_CLI_SUPPORT = NO # Set the SIP_SUPPORT tag to YES if your project consists of sip sources only. # Doxygen will parse them like normal C++ but will assume all classes use public # instead of private inheritance when no explicit protection keyword is present. SIP_SUPPORT = NO # For Microsoft's IDL there are propget and propput attributes to indicate getter # and setter methods for a property. Setting this option to YES (the default) # will make doxygen to replace the get and set methods by a property in the # documentation. This will only work if the methods are indeed getting or # setting a simple type. If this is not the case, or you want to show the # methods anyway, you should set this option to NO. IDL_PROPERTY_SUPPORT = YES # If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC # tag is set to YES, then doxygen will reuse the documentation of the first # member in the group (if any) for the other members of the group. By default # all members of a group must be documented explicitly. DISTRIBUTE_GROUP_DOC = YES # Set the SUBGROUPING tag to YES (the default) to allow class member groups of # the same type (for instance a group of public functions) to be put as a # subgroup of that type (e.g. under the Public Functions section). Set it to # NO to prevent subgrouping. Alternatively, this can be done per class using # the \nosubgrouping command. SUBGROUPING = YES # When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct, union, or enum # is documented as struct, union, or enum with the name of the typedef. So # typedef struct TypeS {} TypeT, will appear in the documentation as a struct # with name TypeT. When disabled the typedef will appear as a member of a file, # namespace, or class. And the struct will be named TypeS. This can typically # be useful for C code in case the coding convention dictates that all compound # types are typedef'ed and only the typedef is referenced, never the tag name. TYPEDEF_HIDES_STRUCT = YES # The SYMBOL_CACHE_SIZE determines the size of the internal cache use to # determine which symbols to keep in memory and which to flush to disk. # When the cache is full, less often used symbols will be written to disk. # For small to medium size projects (<1000 input files) the default value is # probably good enough. For larger projects a too small cache size can cause # doxygen to be busy swapping symbols to and from disk most of the time # causing a significant performance penality. # If the system has enough physical memory increasing the cache will improve the # performance by keeping more symbols in memory. Note that the value works on # a logarithmic scale so increasing the size by one will rougly double the # memory usage. The cache size is given by this formula: # 2^(16+SYMBOL_CACHE_SIZE). The valid range is 0..9, the default is 0, # corresponding to a cache size of 2^16 = 65536 symbols SYMBOL_CACHE_SIZE = 0 #--------------------------------------------------------------------------- # Build related configuration options #--------------------------------------------------------------------------- # If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in # documentation are documented, even if no documentation was available. # Private class members and static file members will be hidden unless # the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES EXTRACT_ALL = NO # If the EXTRACT_PRIVATE tag is set to YES all private members of a class # will be included in the documentation. EXTRACT_PRIVATE = NO # If the EXTRACT_STATIC tag is set to YES all static members of a file # will be included in the documentation. EXTRACT_STATIC = YES # If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) # defined locally in source files will be included in the documentation. # If set to NO only classes defined in header files are included. EXTRACT_LOCAL_CLASSES = NO # This flag is only useful for Objective-C code. When set to YES local # methods, which are defined in the implementation section but not in # the interface are included in the documentation. # If set to NO (the default) only methods in the interface are included. EXTRACT_LOCAL_METHODS = NO # If this flag is set to YES, the members of anonymous namespaces will be # extracted and appear in the documentation as a namespace called # 'anonymous_namespace{file}', where file will be replaced with the base # name of the file that contains the anonymous namespace. By default # anonymous namespace are hidden. EXTRACT_ANON_NSPACES = NO # If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all # undocumented members of documented classes, files or namespaces. # If set to NO (the default) these members will be included in the # various overviews, but no documentation section is generated. # This option has no effect if EXTRACT_ALL is enabled. HIDE_UNDOC_MEMBERS = NO # If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all # undocumented classes that are normally visible in the class hierarchy. # If set to NO (the default) these classes will be included in the various # overviews. This option has no effect if EXTRACT_ALL is enabled. HIDE_UNDOC_CLASSES = NO # If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all # friend (class|struct|union) declarations. # If set to NO (the default) these declarations will be included in the # documentation. HIDE_FRIEND_COMPOUNDS = YES # If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any # documentation blocks found inside the body of a function. # If set to NO (the default) these blocks will be appended to the # function's detailed documentation block. HIDE_IN_BODY_DOCS = NO # The INTERNAL_DOCS tag determines if documentation # that is typed after a \internal command is included. If the tag is set # to NO (the default) then the documentation will be excluded. # Set it to YES to include the internal documentation. INTERNAL_DOCS = NO # If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate # file names in lower-case letters. If set to YES upper-case letters are also # allowed. This is useful if you have classes or files whose names only differ # in case and if your file system supports case sensitive file names. Windows # and Mac users are advised to set this option to NO. CASE_SENSE_NAMES = NO # If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen # will show members with their full class and namespace scopes in the # documentation. If set to YES the scope will be hidden. HIDE_SCOPE_NAMES = YES # If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen # will put a list of the files that are included by a file in the documentation # of that file. SHOW_INCLUDE_FILES = YES # If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen # will list include files with double quotes in the documentation # rather than with sharp brackets. FORCE_LOCAL_INCLUDES = NO # If the INLINE_INFO tag is set to YES (the default) then a tag [inline] # is inserted in the documentation for inline members. INLINE_INFO = NO # If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen # will sort the (detailed) documentation of file and class members # alphabetically by member name. If set to NO the members will appear in # declaration order. SORT_MEMBER_DOCS = NO # If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the # brief documentation of file, namespace and class members alphabetically # by member name. If set to NO (the default) the members will appear in # declaration order. SORT_BRIEF_DOCS = NO # If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen # will sort the (brief and detailed) documentation of class members so that # constructors and destructors are listed first. If set to NO (the default) # the constructors will appear in the respective orders defined by # SORT_MEMBER_DOCS and SORT_BRIEF_DOCS. # This tag will be ignored for brief docs if SORT_BRIEF_DOCS is set to NO # and ignored for detailed docs if SORT_MEMBER_DOCS is set to NO. SORT_MEMBERS_CTORS_1ST = YES # If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the # hierarchy of group names into alphabetical order. If set to NO (the default) # the group names will appear in their defined order. SORT_GROUP_NAMES = NO # If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be # sorted by fully-qualified names, including namespaces. If set to # NO (the default), the class list will be sorted only by class name, # not including the namespace part. # Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. # Note: This option applies only to the class list, not to the # alphabetical list. SORT_BY_SCOPE_NAME = NO # The GENERATE_TODOLIST tag can be used to enable (YES) or # disable (NO) the todo list. This list is created by putting \todo # commands in the documentation. GENERATE_TODOLIST = YES # The GENERATE_TESTLIST tag can be used to enable (YES) or # disable (NO) the test list. This list is created by putting \test # commands in the documentation. GENERATE_TESTLIST = NO # The GENERATE_BUGLIST tag can be used to enable (YES) or # disable (NO) the bug list. This list is created by putting \bug # commands in the documentation. GENERATE_BUGLIST = NO # The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or # disable (NO) the deprecated list. This list is created by putting # \deprecated commands in the documentation. GENERATE_DEPRECATEDLIST= YES # The ENABLED_SECTIONS tag can be used to enable conditional # documentation sections, marked by \if sectionname ... \endif. ENABLED_SECTIONS = # The MAX_INITIALIZER_LINES tag determines the maximum number of lines # the initial value of a variable or define consists of for it to appear in # the documentation. If the initializer consists of more lines than specified # here it will be hidden. Use a value of 0 to hide initializers completely. # The appearance of the initializer of individual variables and defines in the # documentation can be controlled using \showinitializer or \hideinitializer # command in the documentation regardless of this setting. MAX_INITIALIZER_LINES = 30 # Set the SHOW_USED_FILES tag to NO to disable the list of files generated # at the bottom of the documentation of classes and structs. If set to YES the # list will mention the files that were used to generate the documentation. SHOW_USED_FILES = YES # If the sources in your project are distributed over multiple directories # then setting the SHOW_DIRECTORIES tag to YES will show the directory hierarchy # in the documentation. The default is NO. SHOW_DIRECTORIES = YES # Set the SHOW_FILES tag to NO to disable the generation of the Files page. # This will remove the Files entry from the Quick Index and from the # Folder Tree View (if specified). The default is YES. SHOW_FILES = YES # Set the SHOW_NAMESPACES tag to NO to disable the generation of the # Namespaces page. This will remove the Namespaces entry from the Quick Index # and from the Folder Tree View (if specified). The default is YES. SHOW_NAMESPACES = YES # The FILE_VERSION_FILTER tag can be used to specify a program or script that # doxygen should invoke to get the current version for each file (typically from # the version control system). Doxygen will invoke the program by executing (via # popen()) the command , where is the value of # the FILE_VERSION_FILTER tag, and is the name of an input file # provided by doxygen. Whatever the program writes to standard output # is used as the file version. See the manual for examples. FILE_VERSION_FILTER = # The LAYOUT_FILE tag can be used to specify a layout file which will be parsed # by doxygen. The layout file controls the global structure of the generated # output files in an output format independent way. The create the layout file # that represents doxygen's defaults, run doxygen with the -l option. # You can optionally specify a file name after the option, if omitted # DoxygenLayout.xml will be used as the name of the layout file. LAYOUT_FILE = #--------------------------------------------------------------------------- # configuration options related to warning and progress messages #--------------------------------------------------------------------------- # The QUIET tag can be used to turn on/off the messages that are generated # by doxygen. Possible values are YES and NO. If left blank NO is used. QUIET = NO # The WARNINGS tag can be used to turn on/off the warning messages that are # generated by doxygen. Possible values are YES and NO. If left blank # NO is used. WARNINGS = YES # If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings # for undocumented members. If EXTRACT_ALL is set to YES then this flag will # automatically be disabled. WARN_IF_UNDOCUMENTED = YES # If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for # potential errors in the documentation, such as not documenting some # parameters in a documented function, or documenting parameters that # don't exist or using markup commands wrongly. WARN_IF_DOC_ERROR = YES # This WARN_NO_PARAMDOC option can be abled to get warnings for # functions that are documented, but have no documentation for their parameters # or return value. If set to NO (the default) doxygen will only warn about # wrong or incomplete parameter documentation, but not about the absence of # documentation. WARN_NO_PARAMDOC = NO # The WARN_FORMAT tag determines the format of the warning messages that # doxygen can produce. The string should contain the $file, $line, and $text # tags, which will be replaced by the file and line number from which the # warning originated and the warning text. Optionally the format may contain # $version, which will be replaced by the version of the file (if it could # be obtained via FILE_VERSION_FILTER) WARN_FORMAT = "$file:$line: $text" # The WARN_LOGFILE tag can be used to specify a file to which warning # and error messages should be written. If left blank the output is written # to stderr. WARN_LOGFILE = #--------------------------------------------------------------------------- # configuration options related to the input files #--------------------------------------------------------------------------- # The INPUT tag can be used to specify the files and/or directories that contain # documented source files. You may enter file names like "myfile.cpp" or # directories like "/usr/src/myproject". Separate the files or directories # with spaces. INPUT = @doxygen_source_dirs@ # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is # also the default input encoding. Doxygen uses libiconv (or the iconv built # into libc) for the transcoding. See http://www.gnu.org/software/libiconv for # the list of possible encodings. INPUT_ENCODING = UTF-8 # If the value of the INPUT tag contains directories, you can use the # FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp # and *.h) to filter out the source-files in the directories. If left # blank the following patterns are tested: # *.c *.cc *.cxx *.cpp *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh *.hxx # *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.py *.f90 FILE_PATTERNS = *.h \ *.dox # The RECURSIVE tag can be used to turn specify whether or not subdirectories # should be searched for input files as well. Possible values are YES and NO. # If left blank NO is used. RECURSIVE = YES # The EXCLUDE tag can be used to specify files and/or directories that should # excluded from the INPUT source files. This way you can easily exclude a # subdirectory from a directory tree whose root is specified with the INPUT tag. EXCLUDE = # The EXCLUDE_SYMLINKS tag can be used select whether or not files or # directories that are symbolic links (a Unix filesystem feature) are excluded # from the input. EXCLUDE_SYMLINKS = NO # If the value of the INPUT tag contains directories, you can use the # EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude # certain files from those directories. Note that the wildcards are matched # against the file with absolute path, so to exclude all test directories # for example use the pattern */test/* EXCLUDE_PATTERNS = moc_*.cpp */testing/* # The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names # (namespaces, classes, functions, etc.) that should be excluded from the # output. The symbol name can be a fully qualified name, a word, or if the # wildcard * is used, a substring. Examples: ANamespace, AClass, # AClass::ANamespace, ANamespace::*Test EXCLUDE_SYMBOLS = # The EXAMPLE_PATH tag can be used to specify one or more files or # directories that contain example code fragments that are included (see # the \include command). EXAMPLE_PATH = # If the value of the EXAMPLE_PATH tag contains directories, you can use the # EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp # and *.h) to filter out the source-files in the directories. If left # blank all files are included. EXAMPLE_PATTERNS = # If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be # searched for input files to be used with the \include or \dontinclude # commands irrespective of the value of the RECURSIVE tag. # Possible values are YES and NO. If left blank NO is used. EXAMPLE_RECURSIVE = NO # The IMAGE_PATH tag can be used to specify one or more files or # directories that contain image that are included in the documentation (see # the \image command). IMAGE_PATH = images # The INPUT_FILTER tag can be used to specify a program that doxygen should # invoke to filter for each input file. Doxygen will invoke the filter program # by executing (via popen()) the command , where # is the value of the INPUT_FILTER tag, and is the name of an # input file. Doxygen will then use the output that the filter program writes # to standard output. If FILTER_PATTERNS is specified, this tag will be # ignored. INPUT_FILTER = # The FILTER_PATTERNS tag can be used to specify filters on a per file pattern # basis. Doxygen will compare the file name with each pattern and apply the # filter if there is a match. The filters are a list of the form: # pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further # info on how filters are used. If FILTER_PATTERNS is empty, INPUT_FILTER # is applied to all files. FILTER_PATTERNS = # If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using # INPUT_FILTER) will be used to filter the input files when producing source # files to browse (i.e. when SOURCE_BROWSER is set to YES). FILTER_SOURCE_FILES = NO #--------------------------------------------------------------------------- # configuration options related to source browsing #--------------------------------------------------------------------------- # If the SOURCE_BROWSER tag is set to YES then a list of source files will # be generated. Documented entities will be cross-referenced with these sources. # Note: To get rid of all source code in the generated output, make sure also # VERBATIM_HEADERS is set to NO. SOURCE_BROWSER = NO # Setting the INLINE_SOURCES tag to YES will include the body # of functions and classes directly in the documentation. INLINE_SOURCES = NO # Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct # doxygen to hide any special comment blocks from generated source code # fragments. Normal C and C++ comments will always remain visible. STRIP_CODE_COMMENTS = YES # If the REFERENCED_BY_RELATION tag is set to YES # then for each documented function all documented # functions referencing it will be listed. REFERENCED_BY_RELATION = NO # If the REFERENCES_RELATION tag is set to YES # then for each documented function all documented entities # called/used by that function will be listed. REFERENCES_RELATION = NO # If the REFERENCES_LINK_SOURCE tag is set to YES (the default) # and SOURCE_BROWSER tag is set to YES, then the hyperlinks from # functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will # link to the source code. Otherwise they will link to the documentation. REFERENCES_LINK_SOURCE = YES # If the USE_HTAGS tag is set to YES then the references to source code # will point to the HTML generated by the htags(1) tool instead of doxygen # built-in source browser. The htags tool is part of GNU's global source # tagging system (see http://www.gnu.org/software/global/global.html). You # will need version 4.8.6 or higher. USE_HTAGS = NO # If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen # will generate a verbatim copy of the header file for each class for # which an include is specified. Set to NO to disable this. VERBATIM_HEADERS = NO #--------------------------------------------------------------------------- # configuration options related to the alphabetical class index #--------------------------------------------------------------------------- # If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index # of all compounds will be generated. Enable this if the project # contains a lot of classes, structs, unions or interfaces. ALPHABETICAL_INDEX = YES # If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then # the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns # in which this list will be split (can be a number in the range [1..20]) COLS_IN_ALPHA_INDEX = 5 # In case all classes in a project start with a common prefix, all # classes will be put under the same header in the alphabetical index. # The IGNORE_PREFIX tag can be used to specify one or more prefixes that # should be ignored while generating the index headers. IGNORE_PREFIX = #--------------------------------------------------------------------------- # configuration options related to the HTML output #--------------------------------------------------------------------------- # If the GENERATE_HTML tag is set to YES (the default) Doxygen will # generate HTML output. GENERATE_HTML = YES # The HTML_OUTPUT tag is used to specify where the HTML docs will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `html' will be used as the default path. HTML_OUTPUT = html # The HTML_FILE_EXTENSION tag can be used to specify the file extension for # each generated HTML page (for example: .htm,.php,.asp). If it is left blank # doxygen will generate files with .html extension. HTML_FILE_EXTENSION = .html # The HTML_HEADER tag can be used to specify a personal HTML header for # each generated HTML page. If it is left blank doxygen will generate a # standard header. HTML_HEADER = # The HTML_FOOTER tag can be used to specify a personal HTML footer for # each generated HTML page. If it is left blank doxygen will generate a # standard footer. HTML_FOOTER = # The HTML_STYLESHEET tag can be used to specify a user-defined cascading # style sheet that is used by each HTML page. It can be used to # fine-tune the look of the HTML output. If the tag is left blank doxygen # will generate a default style sheet. Note that doxygen will try to copy # the style sheet file to the HTML output directory, so don't put your own # stylesheet in the HTML output directory as well, or it will be erased! HTML_STYLESHEET = # The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. # Doxygen will adjust the colors in the stylesheet and background images # according to this color. Hue is specified as an angle on a colorwheel, # see http://en.wikipedia.org/wiki/Hue for more information. # For instance the value 0 represents red, 60 is yellow, 120 is green, # 180 is cyan, 240 is blue, 300 purple, and 360 is red again. # The allowed range is 0 to 359. HTML_COLORSTYLE_HUE = 220 # The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of # the colors in the HTML output. For a value of 0 the output will use # grayscales only. A value of 255 will produce the most vivid colors. HTML_COLORSTYLE_SAT = 100 # The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to # the luminance component of the colors in the HTML output. Values below # 100 gradually make the output lighter, whereas values above 100 make # the output darker. The value divided by 100 is the actual gamma applied, # so 80 represents a gamma of 0.8, The value 220 represents a gamma of 2.2, # and 100 does not change the gamma. HTML_COLORSTYLE_GAMMA = 80 # If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML # page will contain the date and time when the page was generated. Setting # this to NO can help when comparing the output of multiple runs. HTML_TIMESTAMP = YES # If the HTML_ALIGN_MEMBERS tag is set to YES, the members of classes, # files or namespaces will be aligned in HTML using tables. If set to # NO a bullet list will be used. HTML_ALIGN_MEMBERS = YES # If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML # documentation will contain sections that can be hidden and shown after the # page has loaded. For this to work a browser that supports # JavaScript and DHTML is required (for instance Mozilla 1.0+, Firefox # Netscape 6.0+, Internet explorer 5.0+, Konqueror, or Safari). HTML_DYNAMIC_SECTIONS = NO # If the GENERATE_DOCSET tag is set to YES, additional index files # will be generated that can be used as input for Apple's Xcode 3 # integrated development environment, introduced with OSX 10.5 (Leopard). # To create a documentation set, doxygen will generate a Makefile in the # HTML output directory. Running make will produce the docset in that # directory and running "make install" will install the docset in # ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find # it at startup. # See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html # for more information. GENERATE_DOCSET = NO # When GENERATE_DOCSET tag is set to YES, this tag determines the name of the # feed. A documentation feed provides an umbrella under which multiple # documentation sets from a single provider (such as a company or product suite) # can be grouped. DOCSET_FEEDNAME = "Doxygen generated docs" # When GENERATE_DOCSET tag is set to YES, this tag specifies a string that # should uniquely identify the documentation set bundle. This should be a # reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen # will append .docset to the name. DOCSET_BUNDLE_ID = org.doxygen.Project # When GENERATE_PUBLISHER_ID tag specifies a string that should uniquely identify # the documentation publisher. This should be a reverse domain-name style # string, e.g. com.mycompany.MyDocSet.documentation. DOCSET_PUBLISHER_ID = org.doxygen.Publisher # The GENERATE_PUBLISHER_NAME tag identifies the documentation publisher. DOCSET_PUBLISHER_NAME = Publisher # If the GENERATE_HTMLHELP tag is set to YES, additional index files # will be generated that can be used as input for tools like the # Microsoft HTML help workshop to generate a compiled HTML help file (.chm) # of the generated HTML documentation. GENERATE_HTMLHELP = NO # If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can # be used to specify the file name of the resulting .chm file. You # can add a path in front of the file if the result should not be # written to the html output directory. CHM_FILE = # If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can # be used to specify the location (absolute path including file name) of # the HTML help compiler (hhc.exe). If non-empty doxygen will try to run # the HTML help compiler on the generated index.hhp. HHC_LOCATION = # If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag # controls if a separate .chi index file is generated (YES) or that # it should be included in the master .chm file (NO). GENERATE_CHI = NO # If the GENERATE_HTMLHELP tag is set to YES, the CHM_INDEX_ENCODING # is used to encode HtmlHelp index (hhk), content (hhc) and project file # content. CHM_INDEX_ENCODING = # If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag # controls whether a binary table of contents is generated (YES) or a # normal table of contents (NO) in the .chm file. BINARY_TOC = NO # The TOC_EXPAND flag can be set to YES to add extra items for group members # to the contents of the HTML help documentation and to the tree view. TOC_EXPAND = NO # If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and # QHP_VIRTUAL_FOLDER are set, an additional index file will be generated # that can be used as input for Qt's qhelpgenerator to generate a # Qt Compressed Help (.qch) of the generated HTML documentation. GENERATE_QHP = NO # If the QHG_LOCATION tag is specified, the QCH_FILE tag can # be used to specify the file name of the resulting .qch file. # The path specified is relative to the HTML output folder. QCH_FILE = # The QHP_NAMESPACE tag specifies the namespace to use when generating # Qt Help Project output. For more information please see # http://doc.trolltech.com/qthelpproject.html#namespace QHP_NAMESPACE = org.doxygen.Project # The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating # Qt Help Project output. For more information please see # http://doc.trolltech.com/qthelpproject.html#virtual-folders QHP_VIRTUAL_FOLDER = doc # If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to # add. For more information please see # http://doc.trolltech.com/qthelpproject.html#custom-filters QHP_CUST_FILTER_NAME = # The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the # custom filter to add. For more information please see # # Qt Help Project / Custom Filters. QHP_CUST_FILTER_ATTRS = # The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this # project's # filter section matches. # # Qt Help Project / Filter Attributes. QHP_SECT_FILTER_ATTRS = # If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can # be used to specify the location of Qt's qhelpgenerator. # If non-empty doxygen will try to run qhelpgenerator on the generated # .qhp file. QHG_LOCATION = # If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files # will be generated, which together with the HTML files, form an Eclipse help # plugin. To install this plugin and make it available under the help contents # menu in Eclipse, the contents of the directory containing the HTML and XML # files needs to be copied into the plugins directory of eclipse. The name of # the directory within the plugins directory should be the same as # the ECLIPSE_DOC_ID value. After copying Eclipse needs to be restarted before # the help appears. GENERATE_ECLIPSEHELP = NO # A unique identifier for the eclipse help plugin. When installing the plugin # the directory name containing the HTML and XML files should also have # this name. ECLIPSE_DOC_ID = org.doxygen.Project # The DISABLE_INDEX tag can be used to turn on/off the condensed index at # top of each HTML page. The value NO (the default) enables the index and # the value YES disables it. DISABLE_INDEX = NO # This tag can be used to set the number of enum values (range [1..20]) # that doxygen will group on one line in the generated HTML documentation. ENUM_VALUES_PER_LINE = 1 # The GENERATE_TREEVIEW tag is used to specify whether a tree-like index # structure should be generated to display hierarchical information. # If the tag value is set to YES, a side panel will be generated # containing a tree-like index structure (just like the one that # is generated for HTML Help). For this to work a browser that supports # JavaScript, DHTML, CSS and frames is required (i.e. any modern browser). # Windows users are probably better off using the HTML help feature. GENERATE_TREEVIEW = NO # By enabling USE_INLINE_TREES, doxygen will generate the Groups, Directories, # and Class Hierarchy pages using a tree view instead of an ordered list. USE_INLINE_TREES = NO # If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be # used to set the initial width (in pixels) of the frame in which the tree # is shown. TREEVIEW_WIDTH = 250 # When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open # links to external symbols imported via tag files in a separate window. EXT_LINKS_IN_WINDOW = NO # Use this tag to change the font size of Latex formulas included # as images in the HTML documentation. The default is 10. Note that # when you change the font size after a successful doxygen run you need # to manually remove any form_*.png images from the HTML output directory # to force them to be regenerated. FORMULA_FONTSIZE = 14 # Use the FORMULA_TRANPARENT tag to determine whether or not the images # generated for formulas are transparent PNGs. Transparent PNGs are # not supported properly for IE 6.0, but are supported on all modern browsers. # Note that when changing this option you need to delete any form_*.png files # in the HTML output before the changes have effect. FORMULA_TRANSPARENT = YES # When the SEARCHENGINE tag is enabled doxygen will generate a search box # for the HTML output. The underlying search engine uses javascript # and DHTML and should work on any modern browser. Note that when using # HTML help (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets # (GENERATE_DOCSET) there is already a search function so this one should # typically be disabled. For large projects the javascript based search engine # can be slow, then enabling SERVER_BASED_SEARCH may provide a better solution. SEARCHENGINE = YES # When the SERVER_BASED_SEARCH tag is enabled the search engine will be # implemented using a PHP enabled web server instead of at the web client # using Javascript. Doxygen will generate the search PHP script and index # file to put on the web server. The advantage of the server # based approach is that it scales better to large projects and allows # full text search. The disadvances is that it is more difficult to setup # and does not have live searching capabilities. SERVER_BASED_SEARCH = NO #--------------------------------------------------------------------------- # configuration options related to the LaTeX output #--------------------------------------------------------------------------- # If the GENERATE_LATEX tag is set to YES (the default) Doxygen will # generate Latex output. GENERATE_LATEX = NO # The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `latex' will be used as the default path. LATEX_OUTPUT = latex # The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be # invoked. If left blank `latex' will be used as the default command name. # Note that when enabling USE_PDFLATEX this option is only used for # generating bitmaps for formulas in the HTML output, but not in the # Makefile that is written to the output directory. LATEX_CMD_NAME = latex # The MAKEINDEX_CMD_NAME tag can be used to specify the command name to # generate index for LaTeX. If left blank `makeindex' will be used as the # default command name. MAKEINDEX_CMD_NAME = makeindex # If the COMPACT_LATEX tag is set to YES Doxygen generates more compact # LaTeX documents. This may be useful for small projects and may help to # save some trees in general. COMPACT_LATEX = NO # The PAPER_TYPE tag can be used to set the paper type that is used # by the printer. Possible values are: a4, a4wide, letter, legal and # executive. If left blank a4wide will be used. PAPER_TYPE = a4wide # The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX # packages that should be included in the LaTeX output. EXTRA_PACKAGES = # The LATEX_HEADER tag can be used to specify a personal LaTeX header for # the generated latex document. The header should contain everything until # the first chapter. If it is left blank doxygen will generate a # standard header. Notice: only use this tag if you know what you are doing! LATEX_HEADER = # If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated # is prepared for conversion to pdf (using ps2pdf). The pdf file will # contain links (just like the HTML output) instead of page references # This makes the output suitable for online browsing using a pdf viewer. PDF_HYPERLINKS = YES # If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of # plain latex in the generated Makefile. Set this option to YES to get a # higher quality PDF documentation. USE_PDFLATEX = YES # If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. # command to the generated LaTeX files. This will instruct LaTeX to keep # running if errors occur, instead of asking the user for help. # This option is also used when generating formulas in HTML. LATEX_BATCHMODE = NO # If LATEX_HIDE_INDICES is set to YES then doxygen will not # include the index chapters (such as File Index, Compound Index, etc.) # in the output. LATEX_HIDE_INDICES = NO # If LATEX_SOURCE_CODE is set to YES then doxygen will include # source code with syntax highlighting in the LaTeX output. # Note that which sources are shown also depends on other settings # such as SOURCE_BROWSER. LATEX_SOURCE_CODE = NO #--------------------------------------------------------------------------- # configuration options related to the RTF output #--------------------------------------------------------------------------- # If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output # The RTF output is optimized for Word 97 and may not look very pretty with # other RTF readers or editors. GENERATE_RTF = NO # The RTF_OUTPUT tag is used to specify where the RTF docs will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `rtf' will be used as the default path. RTF_OUTPUT = rtf # If the COMPACT_RTF tag is set to YES Doxygen generates more compact # RTF documents. This may be useful for small projects and may help to # save some trees in general. COMPACT_RTF = NO # If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated # will contain hyperlink fields. The RTF file will # contain links (just like the HTML output) instead of page references. # This makes the output suitable for online browsing using WORD or other # programs which support those fields. # Note: wordpad (write) and others do not support links. RTF_HYPERLINKS = NO # Load stylesheet definitions from file. Syntax is similar to doxygen's # config file, i.e. a series of assignments. You only have to provide # replacements, missing definitions are set to their default value. RTF_STYLESHEET_FILE = # Set optional variables used in the generation of an rtf document. # Syntax is similar to doxygen's config file. RTF_EXTENSIONS_FILE = #--------------------------------------------------------------------------- # configuration options related to the man page output #--------------------------------------------------------------------------- # If the GENERATE_MAN tag is set to YES (the default) Doxygen will # generate man pages GENERATE_MAN = NO # The MAN_OUTPUT tag is used to specify where the man pages will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `man' will be used as the default path. MAN_OUTPUT = man # The MAN_EXTENSION tag determines the extension that is added to # the generated man pages (default is the subroutine's section .3) MAN_EXTENSION = .3 # If the MAN_LINKS tag is set to YES and Doxygen generates man output, # then it will generate one additional man file for each entity # documented in the real man page(s). These additional files # only source the real man page, but without them the man command # would be unable to find the correct page. The default is NO. MAN_LINKS = NO #--------------------------------------------------------------------------- # configuration options related to the XML output #--------------------------------------------------------------------------- # If the GENERATE_XML tag is set to YES Doxygen will # generate an XML file that captures the structure of # the code including all documentation. GENERATE_XML = NO # The XML_OUTPUT tag is used to specify where the XML pages will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `xml' will be used as the default path. XML_OUTPUT = xml # The XML_SCHEMA tag can be used to specify an XML schema, # which can be used by a validating XML parser to check the # syntax of the XML files. XML_SCHEMA = # The XML_DTD tag can be used to specify an XML DTD, # which can be used by a validating XML parser to check the # syntax of the XML files. XML_DTD = # If the XML_PROGRAMLISTING tag is set to YES Doxygen will # dump the program listings (including syntax highlighting # and cross-referencing information) to the XML output. Note that # enabling this will significantly increase the size of the XML output. XML_PROGRAMLISTING = YES #--------------------------------------------------------------------------- # configuration options for the AutoGen Definitions output #--------------------------------------------------------------------------- # If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will # generate an AutoGen Definitions (see autogen.sf.net) file # that captures the structure of the code including all # documentation. Note that this feature is still experimental # and incomplete at the moment. GENERATE_AUTOGEN_DEF = NO #--------------------------------------------------------------------------- # configuration options related to the Perl module output #--------------------------------------------------------------------------- # If the GENERATE_PERLMOD tag is set to YES Doxygen will # generate a Perl module file that captures the structure of # the code including all documentation. Note that this # feature is still experimental and incomplete at the # moment. GENERATE_PERLMOD = NO # If the PERLMOD_LATEX tag is set to YES Doxygen will generate # the necessary Makefile rules, Perl scripts and LaTeX code to be able # to generate PDF and DVI output from the Perl module output. PERLMOD_LATEX = NO # If the PERLMOD_PRETTY tag is set to YES the Perl module output will be # nicely formatted so it can be parsed by a human reader. This is useful # if you want to understand what is going on. On the other hand, if this # tag is set to NO the size of the Perl module output will be much smaller # and Perl will parse it just the same. PERLMOD_PRETTY = YES # The names of the make variables in the generated doxyrules.make file # are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. # This is useful so different doxyrules.make files included by the same # Makefile don't overwrite each other's variables. PERLMOD_MAKEVAR_PREFIX = #--------------------------------------------------------------------------- # Configuration options related to the preprocessor #--------------------------------------------------------------------------- # If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will # evaluate all C-preprocessor directives found in the sources and include # files. ENABLE_PREPROCESSING = YES # If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro # names in the source code. If set to NO (the default) only conditional # compilation will be performed. Macro expansion can be done in a controlled # way by setting EXPAND_ONLY_PREDEF to YES. MACRO_EXPANSION = NO # If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES # then the macro expansion is limited to the macros specified with the # PREDEFINED and EXPAND_AS_DEFINED tags. EXPAND_ONLY_PREDEF = NO # If the SEARCH_INCLUDES tag is set to YES (the default) the includes files # in the INCLUDE_PATH (see below) will be search if a #include is found. SEARCH_INCLUDES = NO # The INCLUDE_PATH tag can be used to specify one or more directories that # contain include files that are not input files but should be processed by # the preprocessor. INCLUDE_PATH = # You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard # patterns (like *.h and *.hpp) to filter out the header-files in the # directories. If left blank, the patterns specified with FILE_PATTERNS will # be used. INCLUDE_FILE_PATTERNS = # The PREDEFINED tag can be used to specify one or more macro names that # are defined before the preprocessor is started (similar to the -D option of # gcc). The argument of the tag is a list of macros of the form: name # or name=definition (no spaces). If the definition and the = are # omitted =1 is assumed. To prevent a macro definition from being # undefined via #undef or recursively expanded use the := operator # instead of the = operator. PREDEFINED = # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then # this tag can be used to specify a list of macro names that should be expanded. # The macro definition that is found in the sources will be used. # Use the PREDEFINED tag if you want to use a different macro definition. EXPAND_AS_DEFINED = # If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then # doxygen's preprocessor will remove all function-like macros that are alone # on a line, have an all uppercase name, and do not end with a semicolon. Such # function macros are typically used for boiler-plate code, and will confuse # the parser if not removed. SKIP_FUNCTION_MACROS = YES #--------------------------------------------------------------------------- # Configuration::additions related to external references #--------------------------------------------------------------------------- # The TAGFILES option can be used to specify one or more tagfiles. # Optionally an initial location of the external documentation # can be added for each tagfile. The format of a tag file without # this location is as follows: # TAGFILES = file1 file2 ... # Adding location for the tag files is done as follows: # TAGFILES = file1=loc1 "file2 = loc2" ... # where "loc1" and "loc2" can be relative or absolute paths or # URLs. If a location is present for each tag, the installdox tool # does not have to be run to correct the links. # Note that each tag file must have a unique name # (where the name does NOT include the path) # If a tag file is not located in the directory in which doxygen # is run, you must also specify the path to the tagfile here. TAGFILES = # When a file name is specified after GENERATE_TAGFILE, doxygen will create # a tag file that is based on the input files it reads. GENERATE_TAGFILE = # If the ALLEXTERNALS tag is set to YES all external classes will be listed # in the class index. If set to NO only the inherited external classes # will be listed. ALLEXTERNALS = NO # If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed # in the modules index. If set to NO, only the current project's groups will # be listed. EXTERNAL_GROUPS = YES # The PERL_PATH should be the absolute path and name of the perl script # interpreter (i.e. the result of `which perl'). PERL_PATH = /usr/bin/perl #--------------------------------------------------------------------------- # Configuration options related to the dot tool #--------------------------------------------------------------------------- # If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will # generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base # or super classes. Setting the tag to NO turns the diagrams off. Note that # this option is superseded by the HAVE_DOT option below. This is only a # fallback. It is recommended to install and use dot, since it yields more # powerful graphs. CLASS_DIAGRAMS = YES # You can define message sequence charts within doxygen comments using the \msc # command. Doxygen will then run the mscgen tool (see # http://www.mcternan.me.uk/mscgen/) to produce the chart and insert it in the # documentation. The MSCGEN_PATH tag allows you to specify the directory where # the mscgen tool resides. If left empty the tool is assumed to be found in the # default search path. MSCGEN_PATH = # If set to YES, the inheritance and collaboration graphs will hide # inheritance and usage relations if the target is undocumented # or is not a class. HIDE_UNDOC_RELATIONS = YES # If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is # available from the path. This tool is part of Graphviz, a graph visualization # toolkit from AT&T and Lucent Bell Labs. The other options in this section # have no effect if this option is set to NO (the default) HAVE_DOT = NO # The DOT_NUM_THREADS specifies the number of dot invocations doxygen is # allowed to run in parallel. When set to 0 (the default) doxygen will # base this on the number of processors available in the system. You can set it # explicitly to a value larger than 0 to get control over the balance # between CPU load and processing speed. DOT_NUM_THREADS = 0 # By default doxygen will write a font called FreeSans.ttf to the output # directory and reference it in all dot files that doxygen generates. This # font does not include all possible unicode characters however, so when you need # these (or just want a differently looking font) you can specify the font name # using DOT_FONTNAME. You need need to make sure dot is able to find the font, # which can be done by putting it in a standard location or by setting the # DOTFONTPATH environment variable or by setting DOT_FONTPATH to the directory # containing the font. DOT_FONTNAME = FreeSans # The DOT_FONTSIZE tag can be used to set the size of the font of dot graphs. # The default size is 10pt. DOT_FONTSIZE = 10 # By default doxygen will tell dot to use the output directory to look for the # FreeSans.ttf font (which doxygen will put there itself). If you specify a # different font using DOT_FONTNAME you can set the path where dot # can find it using this tag. DOT_FONTPATH = # If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen # will generate a graph for each documented class showing the direct and # indirect inheritance relations. Setting this tag to YES will force the # the CLASS_DIAGRAMS tag to NO. CLASS_GRAPH = YES # If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen # will generate a graph for each documented class showing the direct and # indirect implementation dependencies (inheritance, containment, and # class references variables) of the class with other documented classes. COLLABORATION_GRAPH = YES # If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen # will generate a graph for groups, showing the direct groups dependencies GROUP_GRAPHS = YES # If the UML_LOOK tag is set to YES doxygen will generate inheritance and # collaboration diagrams in a style similar to the OMG's Unified Modeling # Language. UML_LOOK = NO # If set to YES, the inheritance and collaboration graphs will show the # relations between templates and their instances. TEMPLATE_RELATIONS = NO # If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT # tags are set to YES then doxygen will generate a graph for each documented # file showing the direct and indirect include dependencies of the file with # other documented files. INCLUDE_GRAPH = YES # If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and # HAVE_DOT tags are set to YES then doxygen will generate a graph for each # documented header file showing the documented files that directly or # indirectly include this file. INCLUDED_BY_GRAPH = YES # If the CALL_GRAPH and HAVE_DOT options are set to YES then # doxygen will generate a call dependency graph for every global function # or class method. Note that enabling this option will significantly increase # the time of a run. So in most cases it will be better to enable call graphs # for selected functions only using the \callgraph command. CALL_GRAPH = NO # If the CALLER_GRAPH and HAVE_DOT tags are set to YES then # doxygen will generate a caller dependency graph for every global function # or class method. Note that enabling this option will significantly increase # the time of a run. So in most cases it will be better to enable caller # graphs for selected functions only using the \callergraph command. CALLER_GRAPH = NO # If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen # will graphical hierarchy of all classes instead of a textual one. GRAPHICAL_HIERARCHY = YES # If the DIRECTORY_GRAPH, SHOW_DIRECTORIES and HAVE_DOT tags are set to YES # then doxygen will show the dependencies a directory has on other directories # in a graphical way. The dependency relations are determined by the #include # relations between the files in the directories. DIRECTORY_GRAPH = YES # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images # generated by dot. Possible values are png, jpg, or gif # If left blank png will be used. DOT_IMAGE_FORMAT = png # The tag DOT_PATH can be used to specify the path where the dot tool can be # found. If left blank, it is assumed the dot tool can be found in the path. DOT_PATH = # The DOTFILE_DIRS tag can be used to specify one or more directories that # contain dot files that are included in the documentation (see the # \dotfile command). DOTFILE_DIRS = # The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of # nodes that will be shown in the graph. If the number of nodes in a graph # becomes larger than this value, doxygen will truncate the graph, which is # visualized by representing a node as a red box. Note that doxygen if the # number of direct children of the root node in a graph is already larger than # DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note # that the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. DOT_GRAPH_MAX_NODES = 50 # The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the # graphs generated by dot. A depth value of 3 means that only nodes reachable # from the root by following a path via at most 3 edges will be shown. Nodes # that lay further from the root node will be omitted. Note that setting this # option to 1 or 2 may greatly reduce the computation time needed for large # code bases. Also note that the size of a graph can be further restricted by # DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction. MAX_DOT_GRAPH_DEPTH = 0 # Set the DOT_TRANSPARENT tag to YES to generate images with a transparent # background. This is disabled by default, because dot on Windows does not # seem to support this out of the box. Warning: Depending on the platform used, # enabling this option may lead to badly anti-aliased labels on the edges of # a graph (i.e. they become hard to read). DOT_TRANSPARENT = NO # Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output # files in one run (i.e. multiple -o and -T options on the command line). This # makes dot run faster, but since only newer versions of dot (>1.8.10) # support this, this feature is disabled by default. DOT_MULTI_TARGETS = YES # If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will # generate a legend page explaining the meaning of the various boxes and # arrows in the dot generated graphs. GENERATE_LEGEND = YES # If the DOT_CLEANUP tag is set to YES (the default) Doxygen will # remove the intermediate dot files that are used to generate # the various graphs. DOT_CLEANUP = YES molequeue-0.9.0/docs/mainpage.dox000066400000000000000000000035331323436134600167570ustar00rootroot00000000000000namespace Avogadro { /** @mainpage MoleQueue API Documentation @section molequeue Introduction The MoleQueue project is developed to support execution of command line executables, such as computational chemistry and simulation codes, both locally and remotely interacting with batch schedulers. These are provided as liberally BSD-licensed, open-source reuable components and an extensible system tray resident Qt application. @subsection main Main Classes The main bulk of the MoleQueue code implements a system tray resident Qt 4 application. If you are interested in extending MoleQueue to support new queue types, transports and/or authentication mechanisms these classes will be of most interest. For applications looking to use the local socket JSON-RPC 2.0 based API the client library will be of most interest. - MoleQueue::Queue : Abstract interface class for queues. - MoleQueue::QueueLocal : Implementation of a local queue. - MoleQueue::QueueRemote : Common API for remote queues. - MoleQueue::SshConnection : Interface for Ssh execution and transfer. - MoleQueue::LocalSocketConnectionListener : Listen for local connections. - The main two classes for client connections: - MoleQueue::Client : Qt client class that can be used to manage jobs. - MoleQueue::JsonRpcClient : General JSON-RPC 2.0 client. @section resources Resources This project is developed as part of the Open Chemistry project. Please see the development guide if you would like to contribute to the project. Some key resources include: - Wiki : http://wiki.openchemistry.org/ - Bug tracker : http://projects.openchemistry.org/ - Dashboard : http://cdash.openchemistry.org/index.php?project=MoleQueue - Mailing lists: http://www.openchemistry.org/OpenChemistry/help/mailing.html */ } molequeue-0.9.0/molequeue/000077500000000000000000000000001323436134600155275ustar00rootroot00000000000000molequeue-0.9.0/molequeue/CMakeLists.txt000066400000000000000000000015441323436134600202730ustar00rootroot00000000000000option(MoleQueue_BUILD_APPLICATION "Build the MoleQueue server application" ON) option(MoleQueue_BUILD_CLIENT "Build the MoleQueue client library" ON) option(USE_ZERO_MQ "Build molequeue with ZeroMQ support" OFF) add_subdirectory(servercore) include_directories(${CMAKE_CURRENT_BINARY_DIR}/servercore) # Are we using ZeroMQ if(USE_ZERO_MQ) add_subdirectory(zeromq) include_directories(${CMAKE_CURRENT_BINARY_DIR}/zeromq) endif() add_subdirectory(plugins) # Client library if(MoleQueue_BUILD_CLIENT) add_subdirectory(client) include_directories(${CMAKE_CURRENT_BINARY_DIR}/client) endif() if(MoleQueue_BUILD_APPLICATION) add_subdirectory(app) endif() # Keep "add_subdirectory(lastinstall)" last: fixup_bundle needs to be # *after* all other install(TARGETS and install(FILES calls if(MoleQueue_BUILD_APPLICATION) add_subdirectory(lastinstall) endif() molequeue-0.9.0/molequeue/app/000077500000000000000000000000001323436134600163075ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/CMakeLists.txt000066400000000000000000000136101323436134600210500ustar00rootroot00000000000000set(qt_components Core Widgets Network) if(MoleQueue_USE_EZHPC_UIT) list(APPEND qt_components XmlPatterns) endif() find_package(Qt5 COMPONENTS ${qt_components} REQUIRED) # Provide some simple API to find the plugins, scripts, etc. if(APPLE) # It is a special case, the app bundle logic breaks the relative pathing. set(MoleQueue_LIB_DIR "lib") else() set(MoleQueue_LIB_DIR "${INSTALL_LIBRARY_DIR}") endif() # Find a python 2.x interpreter. find_package(PythonInterp 2 QUIET) include(GenerateExportHeader) include_directories(${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) # multi configuration build? Needed for plugin search path if(CMAKE_CONFIGURATION_TYPES) add_definitions(-DMULTI_CONFIG_BUILD) endif() if(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT WIN32) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") endif() set(mq_srcs aboutdialog.cpp abstractqueuesettingswidget.cpp advancedfilterdialog.cpp actionfactorymanager.cpp addqueuedialog.cpp filebrowsewidget.cpp filespecification.cpp filesystemtools.cpp importprogramdialog.cpp importqueuedialog.cpp job.cpp jobactionfactory.cpp jobactionfactories/killjobactionfactory.cpp jobactionfactories/opendirectoryactionfactory.cpp jobactionfactories/openwithactionfactory.cpp jobactionfactories/removejobactionfactory.cpp jobactionfactories/viewjoblogactionfactory.cpp jobdata.cpp jobitemmodel.cpp jobmanager.cpp jobreferencebase.cpp jobtableproxymodel.cpp jobtablewidget.cpp jobview.cpp localqueuewidget.cpp logentry.cpp logger.cpp logwindow.cpp mainwindow.cpp openwithmanagerdialog.cpp openwithexecutablemodel.cpp openwithpatternmodel.cpp opensshcommand.cpp patterntypedelegate.cpp pluginmanager.cpp program.cpp programconfiguredialog.cpp queue.cpp queuemanager.cpp queuemanagerdialog.cpp queuemanageritemmodel.cpp queueprogramitemmodel.cpp queues/local.cpp queues/pbs.cpp queues/remote.cpp queues/remotessh.cpp queues/sge.cpp queues/slurm.cpp queuesettingsdialog.cpp remotequeuewidget.cpp server.cpp sshcommand.cpp sshcommandfactory.cpp sshconnection.cpp templatekeyworddialog.cpp terminalprocess.cpp) set(ui_files ui/aboutdialog.ui ui/addqueuedialog.ui ui/advancedfilterdialog.ui ui/importprogramdialog.ui ui/importqueuedialog.ui ui/jobtablewidget.ui ui/localqueuewidget.ui ui/logwindow.ui ui/mainwindow.ui ui/openwithmanagerdialog.ui ui/programconfiguredialog.ui ui/queuemanagerdialog.ui ui/queuesettingsdialog.ui ui/remotequeuewidget.ui ui/templatekeyworddialog.ui) if(MoleQueue_USE_EZHPC_UIT) find_package(KDSoap REQUIRED) include_directories(${KDSoap_INCLUDE_DIRS}) kdsoap_generate_wsdl(ezHPC_UIT_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/wsdl/uitapi.wsdl) list(APPEND mq_srcs ${ezHPC_UIT_SRCS} credentialsdialog.cpp queues/uit/authenticatecont.cpp queues/uit/authenticateresponse.cpp queues/uit/authresponseprocessor.cpp queues/uit/compositeiodevice.cpp queues/uit/dirlistinginfo.cpp queues/uit/fileinfo.cpp queues/uit/filestreamingdata.cpp queues/uit/jobevent.cpp queues/uit/jobeventlist.cpp queues/uit/jobsubmissioninfo.cpp queues/uit/kerberoscredentials.cpp queues/uit/messagehandler.cpp queues/queueuit.cpp queues/uit/sslsetup.cpp queues/uit/authenticator.cpp queues/uit/directoryupload.cpp queues/uit/directorycreate.cpp queues/uit/directorydelete.cpp queues/uit/directorydownload.cpp queues/uit/filesystemoperation.cpp queues/uit/requests.cpp queues/uit/session.cpp queues/uit/sessionmanager.cpp queues/uit/userhostassoc.cpp queues/uit/userhostassoclist.cpp uitqueuewidget.cpp wsdl_uitapi.cpp) list(APPEND ui_files ui/uitqueuewidget.ui ui/credentialsdialog.ui) # Disable warnings for KDSoap generate file for most platforms silence_warnings(wsdl_uitapi.cpp) # install the SSL certificates install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/certs DESTINATION ${INSTALL_DATA_DIR}/molequeue) # copy to build tree so things will work when running from build tree file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/certs DESTINATION ${CMAKE_BINARY_DIR}/${INSTALL_DATA_DIR}/molequeue) set(MoleQueue_SSL_CERT_DIR "${INSTALL_DATA_DIR}/molequeue/certs") endif() if(WIN32) list(APPEND mq_srcs puttycommand.cpp) endif() qt5_wrap_ui(ui_srcs ${ui_files}) qt5_add_resources(rcc_srcs queuetray.qrc) add_library(molequeue_static STATIC ${mq_srcs} ${ui_srcs}) qt5_use_modules(molequeue_static ${qt_components}) set_target_properties(molequeue_static PROPERTIES AUTOMOC TRUE) target_link_libraries(molequeue_static MoleQueueServerCore) if(MoleQueue_USE_EZHPC_UIT) target_link_libraries(molequeue_static KDSoap::kdsoap) endif() if(MoleQueue_BUILD_CLIENT) target_link_libraries(molequeue_static MoleQueueClient) endif() set(sources main.cpp) # Handle Mac OS X specific icons etc. if(APPLE) list(APPEND sources icons/molequeue.icns) set(MACOSX_BUNDLE_ICON_FILE molequeue.icns) set(MACOSX_BUNDLE_BUNDLE_VERSION "${MoleQueue_VERSION}") set_source_files_properties(icons/molequeue.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources) elseif(WIN32) list(APPEND sources icons/molequeue.rc) endif() add_executable(molequeue WIN32 MACOSX_BUNDLE ${sources} ${rcc_srcs}) qt5_use_modules(molequeue ${qt_components}) target_link_libraries(molequeue molequeue_static) if(WIN32) target_link_libraries(molequeue Qt5::WinMain) endif() if(APPLE) set_target_properties(molequeue PROPERTIES OUTPUT_NAME ${MACOSX_BUNDLE_NAME}) endif() install(TARGETS molequeue RUNTIME DESTINATION ${INSTALL_RUNTIME_DIR} BUNDLE DESTINATION . ) include_directories("${CMAKE_CURRENT_BINARY_DIR}/client") # Config file for build options. configure_file(molequeueconfig.h.in molequeueconfig.h) # Only run tests if building both client and app: if(ENABLE_TESTING AND MoleQueue_BUILD_CLIENT AND MoleQueue_BUILD_APPLICATION) add_subdirectory(testing) endif() molequeue-0.9.0/molequeue/app/aboutdialog.cpp000066400000000000000000000023051323436134600213050ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2013 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "aboutdialog.h" #include "ui_aboutdialog.h" #include "molequeueconfig.h" namespace MoleQueue { AboutDialog::AboutDialog(QWidget* parent_) : QDialog(parent_), m_ui(new Ui::AboutDialog) { m_ui->setupUi(this); QString html("

" \ "%2" \ "

"); m_ui->version->setText(html.arg("20").arg(MoleQueue_VERSION)); m_ui->qtVersion->setText(html.arg("10").arg(qVersion())); } AboutDialog::~AboutDialog() { delete m_ui; } } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/aboutdialog.h000066400000000000000000000017051323436134600207550ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2013 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_ABOUTDIALOG_H #define MOLEQUEUE_ABOUTDIALOG_H #include namespace Ui { class AboutDialog; } namespace MoleQueue { class AboutDialog : public QDialog { Q_OBJECT public: AboutDialog(QWidget* parent = 0); ~AboutDialog(); private: Ui::AboutDialog *m_ui; }; } // End namespace #endif molequeue-0.9.0/molequeue/app/abstractqueuesettingswidget.cpp000066400000000000000000000015371323436134600246560ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "abstractqueuesettingswidget.h" namespace MoleQueue { AbstractQueueSettingsWidget::AbstractQueueSettingsWidget(QWidget *parentObject) : QWidget(parentObject), m_isDirty(true) { } } // namespace MoleQueue molequeue-0.9.0/molequeue/app/abstractqueuesettingswidget.h000066400000000000000000000034161323436134600243210ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_ABSTRACTQUEUESETTINGSWIDGET_H #define MOLEQUEUE_ABSTRACTQUEUESETTINGSWIDGET_H #include namespace MoleQueue { /// @brief Base interface for custom queue settings widgets. class AbstractQueueSettingsWidget : public QWidget { Q_OBJECT public: explicit AbstractQueueSettingsWidget(QWidget *parentObject = 0); /// Has the GUI been modified from the current Queue state? bool isDirty() const { return m_isDirty; } signals: /** * @brief Emitted when the options change from their initial settings. */ void modified(); public slots: /// Write the information from the GUI to the Queue. Subclasses /// should call setDirty(false) at the end of their implementation. virtual void save() = 0; /// Update the Queue with the current configuration in the GUI. Subclasses /// should call setDirty(false) at the end of their implementation. virtual void reset() = 0; protected slots: void setDirty(bool dirty = true) { m_isDirty = dirty; if (m_isDirty) emit modified(); } protected: bool m_isDirty; }; } // namespace MoleQueue #endif // MOLEQUEUE_ABSTRACTQUEUESETTINGSWIDGET_H molequeue-0.9.0/molequeue/app/actionfactorymanager.cpp000066400000000000000000000054101323436134600232130ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "actionfactorymanager.h" #include "jobactionfactories/openwithactionfactory.h" namespace MoleQueue { ActionFactoryManager *ActionFactoryManager::m_instance = NULL; ActionFactoryManager::ActionFactoryManager() : QObject(NULL), m_server(NULL) { } ActionFactoryManager::~ActionFactoryManager() { qDeleteAll(m_factories); m_instance = NULL; } void ActionFactoryManager::readSettings(QSettings &settings) { settings.beginGroup("ActionFactoryManager"); int numFactories = settings.beginReadArray("openWithActionFactories"); for (int i = 0; i < numFactories; ++i) { settings.setArrayIndex(i); OpenWithActionFactory *newFactory = new OpenWithActionFactory; newFactory->readSettings(settings); addFactory(newFactory); } settings.endArray(); settings.endGroup(); } void ActionFactoryManager::writeSettings(QSettings &settings) const { settings.beginGroup("ActionFactoryManager"); QList factoryList = factoriesOfType(); settings.beginWriteArray("openWithActionFactories", factoryList.size()); for (int i = 0; i < factoryList.size(); ++i) { settings.setArrayIndex(i); factoryList[i]->writeSettings(settings); } settings.endArray(); settings.endGroup(); } ActionFactoryManager *ActionFactoryManager::instance() { if (!m_instance) { m_instance = new ActionFactoryManager; } return m_instance; } void ActionFactoryManager::addFactory(JobActionFactory *newFactory) { if (!m_factories.contains(newFactory)) { newFactory->setServer(server()); m_factories.append(newFactory); } } QList ActionFactoryManager::factories() const { return m_factories; } QList ActionFactoryManager::factories(JobActionFactory::Flags flags) const { QList result; foreach (JobActionFactory *factory, m_factories) { if ((factory->flags() & flags) == flags) result << factory; } return result; } void ActionFactoryManager::removeFactory(JobActionFactory *factory) { m_factories.removeOne(factory); factory->deleteLater(); } } // namespace MoleQueue molequeue-0.9.0/molequeue/app/actionfactorymanager.h000066400000000000000000000062241323436134600226640ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_ACTIONFACTORYMANAGER_H #define MOLEQUEUE_ACTIONFACTORYMANAGER_H #include #include #include "jobactionfactory.h" namespace MoleQueue { class Server; /** * @class ActionFactoryManager actionfactorymanager.h * * @brief Singleton container for JobActionFactory objects. * @author David C. Lonie */ class ActionFactoryManager : public QObject { Q_OBJECT public: /** * @return The singleton instance of the manager. */ static ActionFactoryManager * instance(); ~ActionFactoryManager(); /** Read programmatically constructed factories from @a settings. */ void readSettings(QSettings &settings); /** Write programmatically constructed factories to @a settings. */ void writeSettings(QSettings &settings) const; /** Set the Server object used by owned factories. */ void setServer(Server *s) { m_server = s; } /** Get the Server object used by owned factories. */ Server * server() const { return m_server; } /** * Add a factory to the manager. The Manager takes ownership of the factory * and sets the server ivar of the factor to the * ActionFactoryManager::server() instance. */ void addFactory(JobActionFactory *); /** * @return A list of all factories owned by the manager. */ QList factories() const; /** * Obtain a subset of owned factories. Factories whose * JobActionFactory::flags() method returns a superset of @a flags will be * returned. * @param flags A combination of JobActionFactory::Flags used to filter the * returned list. * @return A list of JobActionFactory pointers filtered by @a flags. */ QList factories(JobActionFactory::Flags flags) const; /** * Get all factories of a specific type. * @param FactoryType A subclass of JobActionFactory. * @return A list of FactoryType pointers. */ template QList factoriesOfType() const { QList result; foreach (JobActionFactory *factory, m_factories) { if (FactoryType *f = qobject_cast(factory)) { result << f; } } return result; } /** * Remove the factory pointed to by @a factory from the manager. */ void removeFactory(JobActionFactory *factory); protected: ActionFactoryManager(); static ActionFactoryManager *m_instance; Server *m_server; QList m_factories; }; } // namespace MoleQueue #endif // MOLEQUEUE_ACTIONFACTORYMANAGER_H molequeue-0.9.0/molequeue/app/addqueuedialog.cpp000066400000000000000000000044121323436134600217710ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "addqueuedialog.h" #include "ui_addqueuedialog.h" #include "queue.h" #include "queuemanager.h" #include #include namespace MoleQueue { AddQueueDialog::AddQueueDialog(QueueManager *queueManager, QWidget *parentObject) : QDialog(parentObject), ui(new Ui::AddQueueDialog), m_queueManager(queueManager) { ui->setupUi(this); foreach (const QString &queueName, QueueManager::availableQueues()) ui->typeComboBox->addItem(queueName); // Restrict queue names to alphanumeric strings with internal whitespace // (the input is trimmed() in accept()). ui->nameLineEdit->setValidator(new QRegExpValidator( QRegExp(VALID_NAME_REG_EXP))); } AddQueueDialog::~AddQueueDialog() { delete ui; } void AddQueueDialog::accept() { const QString name = ui->nameLineEdit->text().trimmed(); if (name.isEmpty()) { QMessageBox::critical(this, tr("Missing name"), tr("Please enter a name for the queue before " "continuing."), QMessageBox::Ok); return; } const QString type = ui->typeComboBox->currentText(); Queue *queue = m_queueManager->addQueue(name, type); if (queue) { QDialog::accept(); return; } // Queue could not be added. Inform user: QMessageBox::critical(this, tr("Cannot add queue"), tr("Cannot add queue with queue name '%1', as an " "existing queue already has this name. Please rename" " it and try again.").arg(name)); } } // end MoleQueue namespace molequeue-0.9.0/molequeue/app/addqueuedialog.h000066400000000000000000000022651323436134600214420ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef ADDQUEUEDIALOG_H #define ADDQUEUEDIALOG_H #include namespace Ui { class AddQueueDialog; } namespace MoleQueue { class QueueManager; /// @brief Dialog for adding a new queue to the queuemanager. class AddQueueDialog : public QDialog { Q_OBJECT public: explicit AddQueueDialog(QueueManager *queueManager, QWidget *parentObject = 0); ~AddQueueDialog(); public slots: virtual void accept(); private: Ui::AddQueueDialog *ui; QueueManager *m_queueManager; }; } // end MoleQueue namespace #endif // ADDQUEUEDIALOG_H molequeue-0.9.0/molequeue/app/advancedfilterdialog.cpp000066400000000000000000000076561323436134600231640ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "advancedfilterdialog.h" #include "ui_advancedfilterdialog.h" #include "jobtableproxymodel.h" namespace MoleQueue { AdvancedFilterDialog::AdvancedFilterDialog(JobTableProxyModel *model, QWidget *par) : QDialog(par), ui(new Ui::AdvancedFilterDialog), m_proxyModel(model) { ui->setupUi(this); ui->filterStatusNew->setChecked(m_proxyModel->showStatusNew()); ui->filterStatusSubmitted->setChecked(m_proxyModel->showStatusSubmitted()); ui->filterStatusQueued->setChecked(m_proxyModel->showStatusQueued()); ui->filterStatusRunning->setChecked(m_proxyModel->showStatusRunning()); ui->filterStatusFinished->setChecked(m_proxyModel->showStatusFinished()); ui->filterStatusCanceled->setChecked(m_proxyModel->showStatusCanceled()); ui->filterStatusError->setChecked(m_proxyModel->showStatusError()); ui->filterShowHidden->setChecked(m_proxyModel->showHiddenJobs()); connect(ui->filterStatusAll, SIGNAL(clicked()), this, SLOT(selectAllStatuses())); connect(ui->filterStatusNone, SIGNAL(clicked()), this, SLOT(selectNoStatuses())); connect(ui->filterStatusNew, SIGNAL(toggled(bool)), this, SLOT(updateFilters())); connect(ui->filterStatusSubmitted, SIGNAL(toggled(bool)), this, SLOT(updateFilters())); connect(ui->filterStatusQueued, SIGNAL(toggled(bool)), this, SLOT(updateFilters())); connect(ui->filterStatusRunning, SIGNAL(toggled(bool)), this, SLOT(updateFilters())); connect(ui->filterStatusFinished, SIGNAL(toggled(bool)), this, SLOT(updateFilters())); connect(ui->filterStatusCanceled, SIGNAL(toggled(bool)), this, SLOT(updateFilters())); connect(ui->filterStatusError, SIGNAL(toggled(bool)), this, SLOT(updateFilters())); connect(ui->filterShowHidden, SIGNAL(toggled(bool)), this, SLOT(updateFilters())); } AdvancedFilterDialog::~AdvancedFilterDialog() { delete ui; } void AdvancedFilterDialog::updateFilters() { m_proxyModel->setShowStatusNew(ui->filterStatusNew->isChecked()); m_proxyModel->setShowStatusSubmitted(ui->filterStatusSubmitted->isChecked()); m_proxyModel->setShowStatusQueued(ui->filterStatusQueued->isChecked()); m_proxyModel->setShowStatusRunning(ui->filterStatusRunning->isChecked()); m_proxyModel->setShowStatusFinished(ui->filterStatusFinished->isChecked()); m_proxyModel->setShowStatusCanceled(ui->filterStatusCanceled->isChecked()); m_proxyModel->setShowStatusError(ui->filterStatusError->isChecked()); m_proxyModel->setShowHiddenJobs(ui->filterShowHidden->isChecked()); } void AdvancedFilterDialog::selectAllStatuses() { ui->filterStatusNew->setChecked(true); ui->filterStatusSubmitted->setChecked(true); ui->filterStatusQueued->setChecked(true); ui->filterStatusRunning->setChecked(true); ui->filterStatusFinished->setChecked(true); ui->filterStatusCanceled->setChecked(true); ui->filterStatusError->setChecked(true); } void AdvancedFilterDialog::selectNoStatuses() { ui->filterStatusNew->setChecked(false); ui->filterStatusSubmitted->setChecked(false); ui->filterStatusQueued->setChecked(false); ui->filterStatusRunning->setChecked(false); ui->filterStatusFinished->setChecked(false); ui->filterStatusCanceled->setChecked(false); ui->filterStatusError->setChecked(false); } } // namespace MoleQueue molequeue-0.9.0/molequeue/app/advancedfilterdialog.h000066400000000000000000000024541323436134600226200ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_ADVANCEDFILTERDIALOG_H #define MOLEQUEUE_ADVANCEDFILTERDIALOG_H #include namespace Ui { class AdvancedFilterDialog; } namespace MoleQueue { class JobTableProxyModel; /// @brief Provides advanced filtering options for the JobView. class AdvancedFilterDialog : public QDialog { Q_OBJECT public: AdvancedFilterDialog(JobTableProxyModel *model, QWidget *par = 0); ~AdvancedFilterDialog(); protected slots: void updateFilters(); void selectAllStatuses(); void selectNoStatuses(); protected: Ui::AdvancedFilterDialog *ui; JobTableProxyModel *m_proxyModel; }; } // namespace MoleQueue #endif // MOLEQUEUE_ADVANCEDFILTERDIALOG_H molequeue-0.9.0/molequeue/app/certs/000077500000000000000000000000001323436134600174275ustar00rootroot00000000000000Builtin Object Token-Verisign Class 3 Public Primary Certification Authority - G3.cer000066400000000000000000000027441323436134600365340ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/certs-----BEGIN CERTIFICATE----- MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te 2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC /Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== -----END CERTIFICATE----- molequeue-0.9.0/molequeue/app/certs/DOD CA-22.cer000066400000000000000000000037341323436134600212240ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFiTCCBHGgAwIBAgIBSDANBgkqhkiG9w0BAQUFADBbMQswCQYDVQQGEwJVUzEY MBYGA1UEChMPVS5TLiBHb3Zlcm5tZW50MQwwCgYDVQQLEwNEb0QxDDAKBgNVBAsT A1BLSTEWMBQGA1UEAxMNRG9EIFJvb3QgQ0EgMjAeFw0wOTAxMjYyMDE4NTlaFw0x NTAxMjUyMDE4NTlaMFcxCzAJBgNVBAYTAlVTMRgwFgYDVQQKEw9VLlMuIEdvdmVy bm1lbnQxDDAKBgNVBAsTA0RvRDEMMAoGA1UECxMDUEtJMRIwEAYDVQQDEwlET0Qg Q0EtMjIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCb/OGrH/FwNEUF Xwn8HNfVJpPSkGmzHs7YElNwlEIM/KUuzn++aISDhCyPHeLfp9sF1SPzoYd41Cq+ MXVIwvcwa0sVJTyYC8cQLVXPKHazu0MgcqLDAWES3uquvdLklg567ZRhJPutmdri ZhXN1bt374FPYS3PqatVGOhav4mNKc4gW0ATMVaSYEEGywqhM/5uS49bHV4pl+OB 9L3pBD3RMsagbcCThwEXQYcBwiMtsf6waQfIwp8TyoRt0f1yv76avWpgc1aIOsat G8QXvQ0b41Jj/K/B+8wvbjXS3TrYENHEKLe2bP+T4PZy8CkTZws4PBkojWwZk0k9 Wz2XhNcdAgMBAAGjggJaMIICVjAOBgNVHQ8BAf8EBAMCAYYwHwYDVR0jBBgwFoAU SXS7DF66ev4CVO97oMaVxgmAcJYwHQYDVR0OBBYEFCgwH1FRjtXdraHLIMJYFUYw pkRPMAwGA1UdJAQFMAOAAQAwEgYDVR0TAQH/BAgwBgEB/wIBADCBnwYDVR0gBIGX MIGUMAsGCWCGSAFlAgELBTALBglghkgBZQIBCwkwCwYJYIZIAWUCAQsKMAsGCWCG SAFlAgELEjALBglghkgBZQIBCxMwCwYJYIZIAWUCAQsUMAwGCmCGSAFlAwIBAwYw DAYKYIZIAWUDAgEDBzAMBgpghkgBZQMCAQMIMAwGCmCGSAFlAwIBAw0wDAYKYIZI AWUDAgEDETA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vY3JsLmRpc2EubWlsL2dl dGNybD9Eb0QlMjBSb290JTIwQ0ElMjAyMIH+BggrBgEFBQcBAQSB8TCB7jA/Bggr BgEFBQcwAoYzaHR0cDovL2NybC5kaXNhLm1pbC9nZXRJc3N1ZWRUbz9Eb0QlMjBS b290JTIwQ0ElMjAyMCAGCCsGAQUFBzABhhRodHRwOi8vb2NzcC5kaXNhLm1pbDCB iAYIKwYBBQUHMAKGfGxkYXA6Ly9jcmwuZ2RzLmRpc2EubWlsL2NuJTNkRG9EJTIw Um9vdCUyMENBJTIwMiUyY291JTNkUEtJJTJjb3UlM2REb0QlMmNvJTNkVS5TLiUy MEdvdmVybm1lbnQlMmNjJTNkVVM/Y0FDZXJ0aWZpY2F0ZTtiaW5hcnkwDQYJKoZI hvcNAQEFBQADggEBAKfeVjVjzvm0/tj/uSwN7p62qFbVQf0mfmf8spCNq9k45ndV zTeoXrnXvGMkh5HOu5e9mOjlOFfO+w4zbbSUme+5QdilGBYB7v/mvOz4BtHUwWoA 9u24b97jC5hUG4ABnc2hR88OM88oibJJ+nuG/J7iyZaeOLEfJLPMFAWyYzhRazlo Sb+ZgnNZE+HdRtIq87pkCVGflrq6ZrO44ZwT9IbkQQsoet2V2nU3sK/4Z77xrDxH 7GLw0zYJc0UX+L4qFpu8fodFHMPZyetLJ81GrVe2vsA1qBL6EUjbxNrx6ur0D0D8 bteeV3V3vKwMl+xSDr6nmLV4fnzWxZ89fCOn/yU= -----END CERTIFICATE----- molequeue-0.9.0/molequeue/app/certs/DoD Interoperability Root CA 1.cer000066400000000000000000000051021323436134600253650ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIHSTCCBjGgAwIBAgICAl4wDQYJKoZIhvcNAQEFBQAwVjELMAkGA1UEBhMCVVMx GDAWBgNVBAoTD1UuUy4gR292ZXJubWVudDENMAsGA1UECxMERlBLSTEeMBwGA1UE AxMVU0hBLTEgRmVkZXJhbCBSb290IENBMB4XDTExMDMwMzE4NDQwNFoXDTE0MDEw MTA0NTk1OVowbDELMAkGA1UEBhMCVVMxGDAWBgNVBAoTD1UuUy4gR292ZXJubWVu dDEMMAoGA1UECxMDRG9EMQwwCgYDVQQLEwNQS0kxJzAlBgNVBAMTHkRvRCBJbnRl cm9wZXJhYmlsaXR5IFJvb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC AQoCggEBAJx9svbNWhpbtzD1Mi8LnprDHKhCMmtsWVdwQjxKqamx3ULgWb54lsD/ e7kjv15JMvEbaXPrxnJH138pv23Wk+e0YeSkfBzwVs3Qs5kkxMF91e7Sg41oaIyO mOVdKTjIprPn7SErWLqDW7z0A5A/YO4rL5bULjePqyy0G6KADTWJR2NERsSqRUN7 wTzSIUOKAnYTbXtXGb+nOHmbX/pv3/phjAfZTfIm7/KQNR/X/XRykCk28W50xNz3 tRvXhXghryDJeqV9DnThjyRsQ9MJ9IOlORIuT9SU+rNfB9sXgyeeB6bKB96vShPu Ny43rJHoLWyCUr0CCK4fCbwKI6BZ/N0CAwEAAaOCBAkwggQFMA8GA1UdEwEB/wQF MAMBAf8wgewGCCsGAQUFBwEBBIHfMIHcMEUGCCsGAQUFBzAChjlodHRwOi8vaHR0 cC5mcGtpLmdvdi9zaGExZnJjYS9jYUNlcnRzSXNzdWVkVG9zaGExZnJjYS5wN2Mw gZIGCCsGAQUFBzAChoGFbGRhcDovL2xkYXAuZnBraS5nb3YvY249U0hBLTElMjBG ZWRlcmFsJTIwUm9vdCUyMENBLG91PUZQS0ksbz1VLlMuJTIwR292ZXJubWVudCxj PVVTP2NBQ2VydGlmaWNhdGU7YmluYXJ5LGNyb3NzQ2VydGlmaWNhdGVQYWlyO2Jp bmFyeTBUBgNVHSEETTBLMBcGCmCGSAFlAwIBAxcGCWCGSAFlAgELEjAXBgpghkgB ZQMCAQMYBglghkgBZQIBCxMwFwYKYIZIAWUDAgEDGQYJYIZIAWUCAQsSMGQGA1Ud HgEB/wRaMFigVjA5pDcwNTELMAkGA1UEBhMCVVMxGDAWBgNVBAoTD1UuUy4gR292 ZXJubWVudDEMMAoGA1UECxMDRG9EMBmkFzAVMRMwEQYKCZImiZPyLGQBGRYDbWls MDMGA1UdIAQsMCowDAYKYIZIAWUDAgEDFzAMBgpghkgBZQMCAQMYMAwGCmCGSAFl AwIBAxkwggECBggrBgEFBQcBCwSB9TCB8jBKBggrBgEFBQcwBYY+aHR0cDovL2Ny bC5kaXNhLm1pbC9pc3N1ZWRieS9ET0RJTlRFUk9QRVJBQklMSVRZUk9PVENBMV9J Qi5wN2MwgaMGCCsGAQUFBzAFhoGWbGRhcDovL2NybC5nZHMuZGlzYS5taWwvY24l M2REb0QlMjBJbnRlcm9wZXJhYmlsaXR5JTIwUm9vdCUyMENBJTIwMSUyY291JTNk UEtJJTJjb3UlM2REb0QlMmNvJTNkVS5TLiUyMEdvdmVybm1lbnQlMmNjJTNkVVM/ Y3Jvc3NDZXJ0aWZpY2F0ZVBhaXI7YmluYXJ5MA4GA1UdDwEB/wQEAwIBBjAfBgNV HSMEGDAWgBSGmlxi/3OT1eGKf0rGiC6fbqCuEjCBuwYDVR0fBIGzMIGwMDCgLqAs hipodHRwOi8vaHR0cC5mcGtpLmdvdi9zaGExZnJjYS9zaGExZnJjYS5jcmwwfKB6 oHiGdmxkYXA6Ly9sZGFwLmZwa2kuZ292L2NuJTNkU0hBLTElMjBGZWRlcmFsJTIw Um9vdCUyMENBLG91JTNkRlBLSSxvJTNkVS5TLiUyMEdvdmVybm1lbnQsYyUzZFVT P2NlcnRpZmljYXRlUmV2b2NhdGlvbkxpc3QwHQYDVR0OBBYEFHaGHt/tAMl+FDF8 W5SCIUlXvnAHMA0GCSqGSIb3DQEBBQUAA4IBAQCTPUP8zVdGKF+zynjWAT5xDu4N CdDg+d0W6dTLkl6Nnvsv80leE4HjlNY51bLjmSwCDadCKEhQ1RhzCDs2xh+YySJv wdeyJRiQl18gFnq2qL4UxqWSKQCBkFXMsBJVC8FZzZuOqXaSZpKmViLSXVv440Yp 6Lwj/xKeD8IkFcn/iLxtQ66uLk7IBnrPfz7QUu2R7USHMFHtlsQqD5iPHX7iFoDz E3UxJqiJZCeYj4UIYeI+cgFGBmTwPBeUNXO1mRNaZ1/RGEg7S8Pr3Jgwzx1P9zvP tt9bUoqwOmw5z8Kn952U9QS0iBng62F54Me/2Xwwwvtb+q9pB3yITR+UrF9F -----END CERTIFICATE----- molequeue-0.9.0/molequeue/app/certs/DoD Root CA 2.cer000066400000000000000000000047341323436134600220720ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIG/TCCBeWgAwIBAgICAIMwDQYJKoZIhvcNAQEFBQAwbDELMAkGA1UEBhMCVVMx GDAWBgNVBAoTD1UuUy4gR292ZXJubWVudDEMMAoGA1UECxMDRG9EMQwwCgYDVQQL EwNQS0kxJzAlBgNVBAMTHkRvRCBJbnRlcm9wZXJhYmlsaXR5IFJvb3QgQ0EgMTAe Fw0xMDA5MDcxNDE0MDdaFw0xMzA5MDYxNDE0MDdaMFsxCzAJBgNVBAYTAlVTMRgw FgYDVQQKEw9VLlMuIEdvdmVybm1lbnQxDDAKBgNVBAsTA0RvRDEMMAoGA1UECxMD UEtJMRYwFAYDVQQDEw1Eb0QgUm9vdCBDQSAyMIIBIjANBgkqhkiG9w0BAQEFAAOC AQ8AMIIBCgKCAQEAwCzB9o07rP8/PNZxvrh0IgfscEEV/KtA4weqwcPYn/7aTDq/ P8jYKHtLNgHArEUlw9IOCo+FGGQQPRoTcCpvjtfcjZOzQQ84Ic2tq8I9KgXTVxE3 Dc2MUfmT48xGSSGOFLTNyxQ+OM1yMe6rEvJl6jQuVl3/7mN1y226kTT8nvP0LRy+ UMRC31mI/2qz+qhsPctWcXEFlrufgOWARVlnQbDrw61gpIB1BhecDvRD4JkOG/t/ 9bPMsoGCsf0ywbi+QaRktWA6WlEwjM7eQSwZR1xJEGS5dKmHQa99brrBuKG/ZTE6 BGf5tbuOkooAY7ix5ow4X4P/UNU7ol1rshDMYwIDAQABo4IDuDCCA7QwHwYDVR0j BBgwFoAUdoYe3+0AyX4UMXxblIIhSVe+cAcwHQYDVR0OBBYEFEl0uwxeunr+AlTv e6DGlcYJgHCWMA4GA1UdDwEB/wQEAwIBBjAMBgNVHSQEBTADgAEAMA8GA1UdEwEB /wQFMAMBAf8wSwYDVR0gBEQwQjALBglghkgBZQIBCwUwCwYJYIZIAWUCAQsJMAsG CWCGSAFlAgELEjALBglghkgBZQIBCxMwDAYKYIZIAWUDAgEDDTCB8AYDVR0fBIHo MIHlMDygOqA4hjZodHRwOi8vY3JsLmRpc2EubWlsL2NybC9ET0RJTlRFUk9QRVJB QklMSVRZUk9PVENBMS5jcmwwgaSggaGggZ6GgZtsZGFwOi8vY3JsLmdkcy5kaXNh Lm1pbC9jbiUzZERvRCUyMEludGVyb3BlcmFiaWxpdHklMjBSb290JTIwQ0ElMjAx JTJjb3UlM2RQS0klMmNvdSUzZERvRCUyY28lM2RVLlMuJTIwR292ZXJubWVudCUy Y2MlM2RVUz9jZXJ0aWZpY2F0ZVJldm9jYXRpb25MaXN0O2JpbmFyeTCCAR8GCCsG AQUFBwEBBIIBETCCAQ0wSgYIKwYBBQUHMAKGPmh0dHA6Ly9jcmwuZGlzYS5taWwv aXNzdWVkdG8vRE9ESU5URVJPUEVSQUJJTElUWVJPT1RDQTFfSVQucDdjMCAGCCsG AQUFBzABhhRodHRwOi8vb2NzcC5kaXNhLm1pbDCBnAYIKwYBBQUHMAKGgY9sZGFw Oi8vY3JsLmdkcy5kaXNhLm1pbC9jbiUzZERvRCUyMEludGVyb3BlcmFiaWxpdHkl MjBSb290JTIwQ0ElMjAxJTJjb3UlM2RQS0klMmNvdSUzZERvRCUyY28lM2RVLlMu JTIwR292ZXJubWVudCUyY2MlM2RVUz9jQUNlcnRpZmljYXRlO2JpbmFyeTCB3wYI KwYBBQUHAQsEgdIwgc8wOgYIKwYBBQUHMAWGLmh0dHA6Ly9jcmwuZGlzYS5taWwv aXNzdWVkYnkvRE9EUk9PVENBMl9JQi5wN2MwgZAGCCsGAQUFBzAFhoGDbGRhcDov L2NybC5nZHMuZGlzYS5taWwvY24lM2REb0QlMjBSb290JTIwQ0ElMjAyJTJjb3Ul M2RQS0klMmNvdSUzZERvRCUyY28lM2RVLlMuJTIwR292ZXJubWVudCUyY2MlM2RV Uz9jcm9zc0NlcnRpZmljYXRlUGFpcjtiaW5hcnkwDQYJKoZIhvcNAQEFBQADggEB ADMej3uGpu/8wG4D6/Ip2QnttjTcE3zzrC4GyK1wYlmWZbPONaVKl4e40/4lC3nv wPZ/0OfqD7TiWMYa7MyZIk2u6sofCLSAl+4yHIYgLnL/bcA5WXcmmNaXDW4odpFv ZsfenQdWQBONeSkoibjaCoTtF4z8llmB7rcIGTFz7JSCce+FXqireN4aVq/7wG3V oPSVJpZmIxqT5brnYGk4uNiwEa50rzHkQRiZYxtCeVrHZdnfbV//KSDLi+VOZygi oeauyp7TwKgW8L9jcnA3TUKxgkZIZyIAbGxjSofvwPGU8dx1fHU4w0+XyeOE8JX8 d2ZLsXV77w9TCYgW2hQGCjI= -----END CERTIFICATE----- molequeue-0.9.0/molequeue/app/certs/SHA-1 Federal Root CA.cer000066400000000000000000000060621323436134600233720ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIIsTCCB5mgAwIBAgIQP5trXmmW/xK0Pl134v7rYDANBgkqhkiG9w0BAQUFADB2 MQswCQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsT FlZlcmlTaWduIFRydXN0IE5ldHdvcmsxLTArBgNVBAMTJFZlcmlTaWduIENsYXNz IDMgU1NQIEludGVybWVkaWF0ZSBDQTAeFw0xMTAxMDcwMDAwMDBaFw0xMzEyMzEy MzU5NTlaMFYxCzAJBgNVBAYTAlVTMRgwFgYDVQQKEw9VLlMuIEdvdmVybm1lbnQx DTALBgNVBAsTBEZQS0kxHjAcBgNVBAMTFVNIQS0xIEZlZGVyYWwgUm9vdCBDQTCC ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKcHngU6XH6x/7CZErplcj7D Mj6PigVMMvgxFbqAOfNmYHiut+iO/mr1YFNkMy7mM0DJISPfarEzTnFO8fxy5K6R mSrBKZqNdP+xMjkCcgmbZcgHEa+yOLXGjv2wGMHqEZ0pZI8oMDjpSBR2HEeT7JZA wcpZhVEMVz+74cLEejsKGftglotlbuUpPcA5V7cArhGuqQeF43I46bpuMwFQy9S/ p0HEP5t4/AGYkfPoLIgiaCLPlEoUWHlDlepAmpCEet1nEwCVSTCUsoUpkKdovX72 NmjTx/mUMVP7ItIrHJKu19ovFX4MY6Dd26VMeILjQ91YgxzD/m5vgfILs2VUkE8C AwEAAaOCBVkwggVVMIIBMQYIKwYBBQUHAQEEggEjMIIBHzBOBggrBgEFBQcwAoZC aHR0cDovL3NzcC1haWEudmVyaXNpZ24uY29tL1ZUTlNTUC9DZXJ0c19pc3N1ZWRf dG9fQ2xhc3MzU1NQQ0EucDdjMIHMBggrBgEFBQcwAoaBv2xkYXA6Ly9zc3AtYWlh LWxkYXAudmVyaXNpZ24uY29tL0NOPVZlcmlTaWduJTIwQ2xhc3MlMjAzJTIwU1NQ JTIwSW50ZXJtZWRpYXRlJTIwQ0EsT1U9VmVyaVNpZ24lMjBUcnVzdCUyME5ldHdv cmssTz0lMjJWZXJpU2lnbiwlMjBJbmMuJTIyLEM9VVM/Y0FDZXJ0aWZpY2F0ZTti aW5hcnksY3Jvc3NDZXJ0aWZpY2F0ZVBhaXI7YmluYXJ5MA8GA1UdEwEB/wQFMAMB Af8wgbUGA1UdIASBrTCBqjAPBg1ghkgBhvhFAQcXAwEGMA8GDWCGSAGG+EUBBxcD AQcwDwYNYIZIAYb4RQEHFwMBCDAPBg1ghkgBhvhFAQcXAwENMA8GDWCGSAGG+EUB BxcDAREwDwYNYIZIAYb4RQEHFwMBFzAPBg1ghkgBhvhFAQcXAwEYMA8GDWCGSAGG +EUBBxcDARkwDwYNYIZIAYb4RQEHFwMBGjAPBg1ghkgBhvhFAQcXAwEbMIH/BgNV HR8EgfcwgfQwgfGgge6ggeuGN2h0dHA6Ly9zc3AtY3JsLnZlcmlzaWduLmNvbS9D bGFzczNTU1BDQS9DbGFzczNTU1BDQS5jcmyGga9sZGFwOi8vc3NwLWNybC1sZGFw LnZlcmlzaWduLmNvbS9DTj1WZXJpU2lnbiUyMENsYXNzJTIwMyUyMFNTUCUyMElu dGVybWVkaWF0ZSUyMENBLE9VPVZlcmlTaWduJTIwVHJ1c3QlMjBOZXR3b3JrLE89 JTIyVmVyaVNpZ24sJTIwSW5jLiUyMixDPVVTP2NlcnRpZmljYXRlUmV2b2NhdGlv bkxpc3Q7YmluYXJ5MA4GA1UdDwEB/wQEAwIBBjCB7AYIKwYBBQUHAQsEgd8wgdww RQYIKwYBBQUHMAWGOWh0dHA6Ly9odHRwLmZwa2kuZ292L3NoYTFmcmNhL2NhQ2Vy dHNJc3N1ZWRCeXNoYTFmcmNhLnA3YzCBkgYIKwYBBQUHMAWGgYVsZGFwOi8vbGRh cC5mcGtpLmdvdi9jbj1TSEEtMSUyMEZlZGVyYWwlMjBSb290JTIwQ0Esb3U9RlBL SSxvPVUuUy4lMjBHb3Zlcm5tZW50LGM9VVM/Y0FDZXJ0aWZpY2F0ZTtiaW5hcnks Y3Jvc3NDZXJ0aWZpY2F0ZVBhaXI7YmluYXJ5MIIBEgYDVR0hBIIBCTCCAQUwGwYN YIZIAYb4RQEHFwMBFwYKYIZIAWUDAgEDFzAbBg1ghkgBhvhFAQcXAwEYBgpghkgB ZQMCAQMYMBsGDWCGSAGG+EUBBxcDARkGCmCGSAFlAwIBAxkwGwYNYIZIAYb4RQEH FwMBGgYKYIZIAWUDAgEDGjAbBg1ghkgBhvhFAQcXAwEbBgpghkgBZQMCAQMbMBsG DWCGSAGG+EUBBxcDAQgGCmCGSAFlAwIBAwMwGwYNYIZIAYb4RQEHFwMBBgYKYIZI AWUDAgEDAzAbBg1ghkgBhvhFAQcXAwEHBgpghkgBZQMCAQMMMBsGDWCGSAGG+EUB BxcDAQ0GCmCGSAFlAwIBAwwwHQYDVR0OBBYEFIaaXGL/c5PV4Yp/SsaILp9uoK4S MB8GA1UdIwQYMBaAFCwx/8HOq/lN6IkVwGry5atCfUL6MA0GCSqGSIb3DQEBBQUA A4IBAQAImfzPqNsu1pSc7/xhN229yJD17XTnpcK5WVGOnxS2hRWnol2hQVFhjd26 9S+AFJ0TF5vEHV0mBho/9S6QtgvlR2g8sY3XvHF078PJbzBtcUUjl2Yi6UCixB1Y +URizjEAUlu5rnIrQ7UNapg1iS4e0zI6WXqJ2iUn2sFi+QXws0ghK+WrZMI9+OXg TwloQHmxkYa2mhDSMt9hO5ZvC9CvF8N1dpASnSNznhSTkxk1eVIsoaBqRmcb30CP tC+6hpNLKnM0idqMEcES/Q4f163H2NPz/J5EeQ4cKOqFwuN1+SPCbVfNLvKC9yDT fkdiqrvYRfc1i9vtpGWzi3dX4wF2 -----END CERTIFICATE----- molequeue-0.9.0/molequeue/app/certs/VeriSign Class 3 SSP Intermediate CA.cer000066400000000000000000000043641323436134600263150ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIGVDCCBTygAwIBAgIQGYH0QFTS4OtUK7v7RciQfjANBgkqhkiG9w0BAQUFADCB yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMTk5OSBWZXJp U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 aG9yaXR5IC0gRzMwHhcNMTEwMTA3MDAwMDAwWhcNMTMxMjMxMjM1OTU5WjB2MQsw CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl cmlTaWduIFRydXN0IE5ldHdvcmsxLTArBgNVBAMTJFZlcmlTaWduIENsYXNzIDMg U1NQIEludGVybWVkaWF0ZSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBANfMaBonchSI7reVYNNe3hhSwUY/fbEmnDwCoonR2MFXsQkP9n8yNaU1nhRT Eovg4zAetI+e0bDAt9/0Lw/n1x/FdiTTPdMN6SxKLqc8z7xql0MZ+MBzyhsstmIB RmJWkGisFFAZ51BYB/k9AfLtHjQnvc1yHYBgo0ySG2a6ejkJd2r6U/dvjgbu2dSj Eo5XJGl//xSSLKs4HPhkuAsdZr2HqPiBwjlFpCd//Fs8he43JBI60+bRSBiUKpQC ssu6oAj2rvKcy2AMTvjIAlz9Iy3B92fB1Q1JxpbWcLochUca7/NFQTkKMaVeBXxy i2D+SFWfuBLtcl7p/kbtwqfiDbMCAwEAAaOCAocwggKDMA8GA1UdEwEB/wQFMAMB Af8wDgYDVR0PAQH/BAQDAgEGMIHoBgNVHSAEgeAwgd0wDwYNYIZIAYb4RQEHFwMB BjAPBg1ghkgBhvhFAQcXAwEHMA8GDWCGSAGG+EUBBxcDAQgwDwYNYIZIAYb4RQEH FwMBDTAPBg1ghkgBhvhFAQcXAwEOMA8GDWCGSAGG+EUBBxcDAQ8wDwYNYIZIAYb4 RQEHFwMBETAPBg1ghkgBhvhFAQcXAwEUMA8GDWCGSAGG+EUBBxcDARcwDwYNYIZI AYb4RQEHFwMBGDAPBg1ghkgBhvhFAQcXAwEZMA8GDWCGSAGG+EUBBxcDARowDwYN YIZIAYb4RQEHFwMBGzA4BgNVHR8EMTAvMC2gK6AphidodHRwOi8vc3NwLWNybC52 ZXJpc2lnbi5jb20vcGNhMy1nMy5jcmwwKAYDVR0RBCEwH6QdMBsxGTAXBgNVBAMT EFZlcmlTaWduTVBLSS0xLTgwHQYDVR0OBBYEFCwx/8HOq/lN6IkVwGry5atCfUL6 MIHxBgNVHSMEgekwgeahgdCkgc0wgcoxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5W ZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazE6 MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9yaXpl ZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNpZ24gQ2xhc3MgMyBQdWJsaWMgUHJp bWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEczghEAm34GSaM+YrnV7pBI cSnvVzANBgkqhkiG9w0BAQUFAAOCAQEAIS19vzG9j+KXiQ0G1bOuJCeiD9KKW1+8 69cutvgDf3hEvrw39Gr2ek3cAdso7dvwW0Z17muzpHV08gWTjjKba8mBzjijmgr9 I2vE2K/Ls72WJvTDUjCAHfBJKeK1q8v7xv1xtf2Jz7BV8sNH3kDB7jhhE++8zLVC gyFilU0KZfhBpLPVlVYnLozRdvsHfNnO/JskJvRqhDYbeC5ginQT0m5sTQiyTYqL /IU+i82TxANXjC7syl0dfcGr8pJ85T9bF1EZLxdgikAYLKPGTuXMwOGqT5bR0dKD lWShiGTRl7HW0KJMg05F0HjOnYpdOYGaFrQghecrkcrRPRevSdFVHQ== -----END CERTIFICATE----- molequeue-0.9.0/molequeue/app/certs/www.uit.hpc.mil.cer000066400000000000000000000033721323436134600231040ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIE5TCCA82gAwIBAgIDAUYiMA0GCSqGSIb3DQEBBQUAMFcxCzAJBgNVBAYTAlVT MRgwFgYDVQQKEw9VLlMuIEdvdmVybm1lbnQxDDAKBgNVBAsTA0RvRDEMMAoGA1UE CxMDUEtJMRIwEAYDVQQDEwlET0QgQ0EtMjIwHhcNMTEwNzI4MTcwMjA5WhcNMTQw NzI4MTcwMjA5WjBrMQswCQYDVQQGEwJVUzEYMBYGA1UEChMPVS5TLiBHb3Zlcm5t ZW50MQwwCgYDVQQLEwNEb0QxDDAKBgNVBAsTA1BLSTEMMAoGA1UECxMDVVNBMRgw FgYDVQQDEw93d3cudWl0LmhwYy5taWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw ggEKAoIBAQDJUNVgiZybjO4SXzhCbQrMqDMtoQEPkw8BATv//wfm7p3Q1GQBBtkc VQe9lNR1PsCMGTUV1siYLWim/IJYlbnoTdEwL9mB5kTUcR9fi67x/qDmrL1fzjM5 XY5j2dlAut/+6nuuLnuD8J1HgbqOnolK56kjF5MmTKSnmdmDaSSQ4yCJ69vOelaT jXehUq64tZAWMnEfxxBMjmHch+aLNGB/x1rG1F//lyMslBeNgWYH2zBkT4SpVh58 JYYT+6dwtexIeWrn5zWYjAc7+3CyRs/PsyoM7IqL43Xsl873MoT43xm2FLvc0fxc 96+vwMQdIEN4lliwNG9mY88YRKFA68cpAgMBAAGjggGkMIIBoDAfBgNVHSMEGDAW gBQoMB9RUY7V3a2hyyDCWBVGMKZETzAdBgNVHQ4EFgQUC6V5W8fqcRJ+nKAwqueP HvxF44MwDgYDVR0PAQH/BAQDAgWgMIHDBgNVHR8EgbswgbgwKqAooCaGJGh0dHA6 Ly9jcmwuZGlzYS5taWwvY3JsL0RPRENBXzIyLmNybDCBiaCBhqCBg4aBgGxkYXA6 Ly9jcmwuZ2RzLmRpc2EubWlsL2NuJTNkRE9EJTIwQ0EtMjIlMmNvdSUzZFBLSSUy Y291JTNkRG9EJTJjbyUzZFUuUy4lMjBHb3Zlcm5tZW50JTJjYyUzZFVTP2NlcnRp ZmljYXRlcmV2b2NhdGlvbmxpc3Q7YmluYXJ5MCMGA1UdIAQcMBowCwYJYIZIAWUC AQsFMAsGCWCGSAFlAgELEjBjBggrBgEFBQcBAQRXMFUwMQYIKwYBBQUHMAKGJWh0 dHA6Ly9jcmwuZGlzYS5taWwvc2lnbi9ET0RDQV8yMi5jZXIwIAYIKwYBBQUHMAGG FGh0dHA6Ly9vY3NwLmRpc2EubWlsMA0GCSqGSIb3DQEBBQUAA4IBAQBG7zeCErcD koTgRnfq6QmobiCMr+5KmQLlfoN+4WXCAEs4flE0WL1Kz9LQbYCe24MeEP6l6Y90 YgFZa3CUy9o4LPQTNDCut3LKmAe83uwg4iJuQZiV9qDUjcaHPN0o8jdsEPcekDLB +4K1wXB5mVzv1F3xAQcCEkDULJZcVRxGnrbB6OKzal53UbVzoy3Xut8QACMsN4q0 w3Tk9J4kTG/MKFNO/wNgdAW7QS70dWtOc1Gc3Zu4u51Pc/JkY0kXh5vGnoRgxyQM iWF67MC88khCKo2aXSxM0yqOZoq3+Xn3yP+I8kngxouvf5oTmwI7/VHSLE0d9NdL +O9tgSJZAiEZ -----END CERTIFICATE----- molequeue-0.9.0/molequeue/app/credentialsdialog.cpp000066400000000000000000000031541323436134600224730ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "credentialsdialog.h" #include "ui_credentialsdialog.h" namespace MoleQueue { CredentialsDialog::CredentialsDialog(QWidget *parentObject) : QDialog(parentObject), ui(new Ui::CredentialsDialog) { ui->setupUi(this); connect(ui->buttonBox, SIGNAL(rejected()), this, SIGNAL(canceled())); } CredentialsDialog::~CredentialsDialog() { delete ui; } void CredentialsDialog::accept() { emit entered(ui->credentialsEdit->text()); ui->credentialsEdit->clear(); ui->messageLabel->clear(); QDialog::accept(); } void CredentialsDialog::reject() { ui->credentialsEdit->clear(); ui->messageLabel->clear(); QDialog::reject(); } void CredentialsDialog::setHostString(const QString &hostString) { ui->hostLabel->setText(hostString); } void CredentialsDialog::setPrompt(const QString &prompt) { ui->promptLabel->setText(prompt); } void CredentialsDialog::setErrorMessage(const QString &message) { ui->messageLabel->setText(message); } } // end namespace MoleQueue molequeue-0.9.0/molequeue/app/credentialsdialog.h000066400000000000000000000026601323436134600221410ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef CREDENTIALSDIALOG_H #define CREDENTIALSDIALOG_H #include namespace Ui { class CredentialsDialog; } namespace MoleQueue { /** * @class CredentialsWidget credentialswidget.h * * @brief A dialog for prompting user for security credentials. */ class CredentialsDialog: public QDialog { Q_OBJECT public: explicit CredentialsDialog(QWidget *parentObject = 0); ~CredentialsDialog(); void setHostString(const QString &hostString); void setPrompt(const QString &prompt); void setErrorMessage(const QString &message); public slots: void accept(); void reject(); signals: void entered(const QString &credentials); void canceled(); private: Ui::CredentialsDialog *ui; }; } // end namespace MoleQueue #endif //CREDENTIALSDIALOG_H molequeue-0.9.0/molequeue/app/filebrowsewidget.cpp000066400000000000000000000123661323436134600223700ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2013 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "filebrowsewidget.h" #include #include #include #include #include #include #include #include #include namespace MoleQueue { FileBrowseWidget::FileBrowseWidget(QWidget *theParent) : QWidget(theParent), m_mode(), // use the setter to initialize filters. m_valid(false), m_fileSystemModel(new QFileSystemModel(this)), m_button(new QPushButton(tr("Browse"))), m_edit(new QLineEdit) { QHBoxLayout *hbox = new QHBoxLayout; hbox->addWidget(m_edit); hbox->addWidget(m_button); setLayout(hbox); // Focus config setFocusPolicy(Qt::StrongFocus); setFocusProxy(m_edit); setTabOrder(m_edit, m_button); // Setup completion m_fileSystemModel->setRootPath(QDir::rootPath()); QCompleter *fsCompleter = new QCompleter(m_fileSystemModel, this); m_edit->setCompleter(fsCompleter); // Connections: connect(m_button, SIGNAL(clicked()), SLOT(browse())); connect(m_edit, SIGNAL(textChanged(QString)), SLOT(testFileName())); connect(m_edit, SIGNAL(textChanged(QString)), SIGNAL(fileNameChanged(QString))); setMode(ExistingFile); } FileBrowseWidget::~FileBrowseWidget() { } QString FileBrowseWidget::fileName() const { return m_edit->text(); } QPushButton *FileBrowseWidget::browseButton() const { return m_button; } QLineEdit *FileBrowseWidget::lineEdit() const { return m_edit; } void FileBrowseWidget::setFileName(const QString &fname) { m_edit->setText(fname); } void FileBrowseWidget::browse() { QString fname(fileName()); QFileInfo info(fname); QString initialFilePath; if (info.isAbsolute()) { initialFilePath = info.absolutePath(); } else if (m_mode == ExecutableFile) { initialFilePath = searchSystemPathForFile(fname); if (!initialFilePath.isEmpty()) initialFilePath = QFileInfo(initialFilePath).absolutePath(); } if (initialFilePath.isEmpty()) initialFilePath = QDir::homePath(); initialFilePath += "/" + info.fileName(); info = QFileInfo(initialFilePath); QFileDialog dlg(this); switch (m_mode) { default: case ExistingFile: dlg.setWindowTitle(tr("Select file:")); break; case ExecutableFile: dlg.setWindowTitle(tr("Select executable:")); dlg.setFilter(QDir::Executable); break; } dlg.setFileMode(QFileDialog::ExistingFile); dlg.setDirectory(info.absolutePath()); dlg.selectFile(info.fileName()); if (static_cast(dlg.exec()) == QFileDialog::Accepted && !dlg.selectedFiles().isEmpty()) setFileName(dlg.selectedFiles().first()); } void FileBrowseWidget::testFileName() { QFileInfo info(fileName()); if (info.isAbsolute()) { if (info.exists()) { if (m_mode != ExecutableFile || info.isExecutable()) { fileNameMatch(); return; } } } else if (m_mode == ExecutableFile) { // for non-absolute executables, search PATH QString absoluteFilePath = searchSystemPathForFile(fileName()); if (!absoluteFilePath.isNull()) { fileNameMatch(); return; } } fileNameNoMatch(); } void FileBrowseWidget::fileNameMatch() { QPalette pal; pal.setColor(QPalette::Text, Qt::black); m_edit->setPalette(pal); m_valid = true; } void FileBrowseWidget::fileNameNoMatch() { QPalette pal; pal.setColor(QPalette::Text, Qt::red); m_edit->setPalette(pal); m_valid = false; } QString FileBrowseWidget::searchSystemPathForFile(const QString &exec) { QString result; QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); if (!env.contains("PATH")) return result; static QRegExp pathSplitter = QRegExp( #ifdef Q_OS_WIN32 ";" #else // WIN32 ":" #endif// WIN32 ); QStringList paths = env.value("PATH").split(pathSplitter, QString::SkipEmptyParts); foreach (const QString &path, paths) { QFileInfo info(path + "/" + exec); if (!info.exists() || !info.isFile()) { continue; } result = info.absoluteFilePath(); break; } return result; } void FileBrowseWidget::setMode(FileBrowseWidget::Mode m) { m_mode = m; QDir::Filters modelFilters = QDir::Files | QDir::AllDirs | QDir::NoDot | QDir::Drives; // This should go here, but unfortunately this also filters out a ton of // directories as well... // if (m_mode == ExecutableFile) // modelFilters |= QDir::Executable; m_fileSystemModel->setFilter(modelFilters); testFileName(); } FileBrowseWidget::Mode FileBrowseWidget::mode() const { return m_mode; } } // namespace MoleQueue molequeue-0.9.0/molequeue/app/filebrowsewidget.h000066400000000000000000000036241323436134600220320ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2013 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_FILEBROWSEWIDGET_H #define MOLEQUEUE_FILEBROWSEWIDGET_H #include class QFileSystemModel; class QLineEdit; class QPushButton; namespace MoleQueue { class FileBrowseWidget : public QWidget { Q_OBJECT public: enum Mode { ExistingFile = 0, ExecutableFile }; explicit FileBrowseWidget(QWidget *theParent = 0); ~FileBrowseWidget(); QString fileName() const; bool validFileName() const { return m_valid; } QPushButton *browseButton() const; QLineEdit *lineEdit() const; void setMode(Mode m); Mode mode() const; signals: void fileNameChanged(const QString &filename); public slots: void setFileName(const QString &fname); private slots: void browse(); void testFileName(); void fileNameMatch(); void fileNameNoMatch(); private: /** * @brief Search the environment variable PATH for a file with the specified * name. * @param exec The name of the file. * @return The absolute path to the file on the system, or a null QString if * not found. */ static QString searchSystemPathForFile(const QString &exec); Mode m_mode; bool m_valid; QFileSystemModel *m_fileSystemModel; QPushButton *m_button; QLineEdit *m_edit; }; } // namespace MoleQueue #endif // MOLEQUEUE_FILEBROWSEWIDGET_H molequeue-0.9.0/molequeue/app/filespecification.cpp000066400000000000000000000125711323436134600225010ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "filespecification.h" #include "logger.h" #include #include #include #include #include namespace MoleQueue { FileSpecification::FileSpecification() { } FileSpecification::FileSpecification(const QJsonObject &json) { m_json = json; } FileSpecification::FileSpecification(const QString &path) { m_json.insert("path", path); } FileSpecification::FileSpecification(const QString &filename_, const QString &contents_) { m_json.insert("filename", filename_); m_json.insert("contents", contents_); } FileSpecification::FileSpecification(QFile *file, FileSpecification::Format format_) { switch (format_) { case PathFileSpecification: m_json.insert("path", QFileInfo(*file).absoluteFilePath()); break; case ContentsFileSpecification: { m_json.insert("filename", QFileInfo(*file).fileName()); if (!file->open(QFile::ReadOnly | QFile::Text)) { Logger::logError(Logger::tr("Error opening file for read: '%1'") .arg(file->fileName())); return; } m_json.insert("contents", QString(file->readAll())); file->close(); break; } case InvalidFileSpecification: Logger::logDebugMessage(Logger::tr("Cannot convert file to invalid file " "spec! (%1)").arg(file->fileName())); break; default: Logger::logDebugMessage(Logger::tr("Unknown filespec format (%1) for file " "'%2'").arg(format_).arg(file->fileName())); break; } } FileSpecification::FileSpecification(const FileSpecification &other) { m_json = other.m_json; } FileSpecification &FileSpecification::operator=(const FileSpecification &other) { m_json = other.m_json; return *this; } FileSpecification::~FileSpecification() { } FileSpecification::Format FileSpecification::format() const { if (m_json.contains("path")) { return PathFileSpecification; } else if (m_json.contains("filename") && m_json.contains("contents")) { return ContentsFileSpecification; } return InvalidFileSpecification; } QByteArray FileSpecification::toJson() const { return QJsonDocument(m_json).toJson(); } QJsonObject FileSpecification::toJsonObject() const { return m_json; } bool FileSpecification::fileExists() const { if (format() == PathFileSpecification) return QFile::exists(m_json.value("path").toString()); return false; } bool FileSpecification::writeFile(const QDir &dir, const QString &filename_) const { switch (format()) { default: case InvalidFileSpecification: return false; case PathFileSpecification: case ContentsFileSpecification: { QString path = dir.absoluteFilePath(filename_.isNull() ? filename() : filename_); QFile file(path); if (!file.open(QFile::WriteOnly | QFile::Truncate | QFile::Text)) return false; file.write(contents().toLocal8Bit()); file.close(); return true; } } } QString FileSpecification::filename() const { switch (format()) { default: case InvalidFileSpecification: Logger::logDebugMessage(Logger::tr("Cannot extract filename from invalid " "filespec\n%1").arg(QString(toJson()))); return QString(); case PathFileSpecification: return QFileInfo(m_json.value("path").toString()).fileName(); case ContentsFileSpecification: return QFileInfo(m_json.value("filename").toString()).fileName(); } } QString FileSpecification::contents() const { switch (format()) { default: case InvalidFileSpecification: Logger::logWarning(Logger::tr("Cannot read contents of invalid filespec:" "\n%1").arg(QString(toJson()))); return QString(); case PathFileSpecification: { QFile file(filepath()); if (!file.open(QFile::ReadOnly | QFile::Text)) { Logger::logError(Logger::tr("Error opening file for read: '%1'") .arg(file.fileName())); return QString(); } QString result = file.readAll(); file.close(); return result; } case ContentsFileSpecification: return QString(m_json.value("contents").toString()); } } QString FileSpecification::filepath() const { if (format() == PathFileSpecification) return QFileInfo(m_json.value("path").toString()).absoluteFilePath(); return QString(); } bool FileSpecification::fileHasExtension() const { return !QFileInfo(filename()).suffix().isEmpty(); } QString FileSpecification::fileBaseName() const { return QFileInfo(filename()).baseName(); } QString FileSpecification::fileExtension() const { return QFileInfo(filename()).suffix(); } } // namespace MoleQueue molequeue-0.9.0/molequeue/app/filespecification.h000066400000000000000000000102221323436134600221350ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_FILESPEC_H #define MOLEQUEUE_FILESPEC_H #include #include #include class QDir; class QFile; namespace MoleQueue { class FileSpecificationPrivate; /** * @class FileSpecification filespecification.h * @brief Specify files for simplifying Client-Server communication. * @author David C. Lonie * * The FileSpecification class contains a description of a file to facilite * file manipulation during RPC communication. Files are stored as either a path * to the local file on disk, or a filename and content string. */ class FileSpecification { public: /// Recognized internal formats for storing file data enum Format { /// Invalid format InvalidFileSpecification = -1, /// Single "path" member pointing to a location on the filesystem PathFileSpecification = 0, /// Filename and content strings. ContentsFileSpecification }; /// Creates an invalid FileSpecification. FileSpecification(); /// Create a FileSpecification using the members of the input QJsonObject. explicit FileSpecification(const QJsonObject &json); /// Create a FileSpecification from the input absolute filepath. explicit FileSpecification(const QString &path); /// Create a FileSpecification from the filename and content strings. FileSpecification(const QString &filename_, const QString &contents_); /// Create a FileSpecification from the specified file using the indicated /// format FileSpecification(QFile *file, Format format_ = PathFileSpecification); /// Copy a FileSpecification FileSpecification(const FileSpecification &other); /// Copy a FileSpecification FileSpecification & operator=(const FileSpecification &other); /// Destroy the FileSpec ~FileSpecification(); /// @return The format of the FileSpec /// @see FileSpecification::Format Format format() const; /// @return True if the FileSpecification is formatted properly, false /// otherwise. bool isValid() const { return format() != InvalidFileSpecification; } /// @return The FileSpecification as a formatted JSON string. QByteArray toJson() const; /// @return The FileSpecification as a formatted JSON string. QJsonObject toJsonObject() const; /// @return Whether or not the FileSpecification refers to an existing file /// @note This will always be false if format() does not return /// ContentsFileSpecification. bool fileExists() const; /// Write contents() to a file with @a filename_ in @a dir. If @a filename /// is not specified, filename() will be used instead (default). /// @return True if the file is successfully written. bool writeFile(const QDir &dir, const QString &filename_ = QString()) const; /// @return The filename (without path) of the FileSpecification. QString filename() const; /// @return The contents of the file. QString contents() const; /// @return The filename (with path) of the FileSpecification. /// @note This function only makes sense if format() is PathFileSpecification. /// It will always return a null string otherwise. QString filepath() const; /// @return True if the filename has an extension ("file.ext"), false /// otherwise ("file") bool fileHasExtension() const; /// @return The filename without an extension. QString fileBaseName() const; /// @return The file extension, if any, or a null string. /// @see hasFileExtension QString fileExtension() const; private: QJsonObject m_json; }; } // namespace MoleQueue #endif // MOLEQUEUE_FILESPEC_H molequeue-0.9.0/molequeue/app/filesystemtools.cpp000066400000000000000000000051631323436134600222650ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "filesystemtools.h" #include #include #include #include namespace MoleQueue { namespace FileSystemTools { bool recursiveRemoveDirectory(const QString &p, bool deleteContentsOnly) { QString path = QDir::cleanPath(p); // Just a safety to prevent accidentally wiping / if (path.isEmpty() || path.simplified() == "/") return false; bool result = true; QDir dir; dir.setPath(path); if (dir.exists()) { foreach (QFileInfo info, dir.entryInfoList( QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst)) { result = info.isDir() ? recursiveRemoveDirectory(info.absoluteFilePath()) : QFile::remove(info.absoluteFilePath()); if (!result) return false; } // Remove the directory if needed if (!deleteContentsOnly) result = dir.rmdir(path); } if (!result) return false; return true; } bool recursiveCopyDirectory(const QString &from, const QString &to) { bool result = true; QDir fromDir; fromDir.setPath(from); if (!fromDir.exists()) return false; QDir toDir; toDir.setPath(to); if (!toDir.exists()) { if (!toDir.mkdir(toDir.absolutePath())) return false; } foreach (QFileInfo info, fromDir.entryInfoList( QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst)) { QString newTargetPath = QString ("%1/%2") .arg(toDir.absolutePath(), fromDir.relativeFilePath(info.absoluteFilePath())); if (info.isDir()) { result = recursiveCopyDirectory(info.absoluteFilePath(), newTargetPath); } else { result = QFile::copy(info.absoluteFilePath(), newTargetPath); } if (!result) return false; } return true; } } // namespace FileSystemTools } // namespace MoleQueue molequeue-0.9.0/molequeue/app/filesystemtools.h000066400000000000000000000023351323436134600217300ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_FILESYSTEMTOOLS_H #define MOLEQUEUE_FILESYSTEMTOOLS_H #include namespace MoleQueue { namespace FileSystemTools { /// Remove the directory at @a path. If @a deleteContentsOnly is true, /// the directory itself will not be removed. bool recursiveRemoveDirectory(const QString &path, bool deleteContentsOnly = false); /// Copy the contents of directory @a from into @a to. bool recursiveCopyDirectory(const QString &from, const QString &to); } // namespace FileSystemTools } // namespace MoleQueue #endif // MOLEQUEUE_FILESYSTEMTOOLS_H molequeue-0.9.0/molequeue/app/icons/000077500000000000000000000000001323436134600174225ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/icons/MoleQueue_About.png000066400000000000000000004634571323436134600232060ustar00rootroot00000000000000‰PNG  IHDRòX¨~XtEXtSoftwareAdobe ImageReadyqÉe<fÑIDATxÚì}œeùÿûÎìîíÞ].Tj!Ô»\Úݤˆ‚†"¢"* (((ê_)?Eýý¥+ˆ *–¤„.½\.!¤’’z¹º·»3ïÿyfÞÙ}wvfÛínîà}òysÛæwÞö}úKcD’$I’$I’Ô5I‘] I’$I’$I —$I’$I’$I —$I’$I’$I —$I’$I’$K’$I’$I’$K’$I’$I’$K’$I’$I’$K’$I’$IÈ%I’$I’$IÈ%I’$I’$IÈ%I’$I’$ ä’$I’$I’$ ä’$I’$I’$ ä’$I’$I’$ ä’$I’$I’rI’$I’$I’rI’$I’$I’rI’$I’$I’rI’$I’$I¹$I’$I’$I¹$I’$I’$)[òêFEŸ —Õ•C©†òÛ<4µ”Pú@)ƒr 6Š—¿> %% å0”ãPŽA9åÄÇebèªôÙù)=²Ñx-I’¤ÎCÛwï’ ©ð@žc åPB©ë@=½ Œ„RÁÿŽ…Ò^’A=-P ìƒ²Êz(k l‚rDN3I’$I’$<žs)ùOP¦CiÎàÚQPj |Êx.}w”Jx9Êdáó8£ñ&”P6È)'I’$I’$rÿ‹ôýPnKñûÓ¡\å \ú.*P;Qºÿ,/¨Ž_å9(ÿ…²[N?I’$I’ÔQêªÎn#„×ßò9—ß¡´þ4”µP‚2£€ n'oσ¼=ÿ ¦_’$I’$IúD9:œ)¼§P%ñ*òó ¼e”¯@éÝÉž¡”ë ÔByÊÅr*J’$I’¤O #ˆ÷´}v—t« Ì‡ò6”K8ÈwvºÊ«ÐgÈ))I’$I’>î@ŽŽnªÃ糈éÅþé.:èè÷g¯q$I’$I’>V@>âc<è|ø5(Ë |CNOI’$I’ôqòQŸ€qA{ÿ“Pf3¤M’$I’$I:¹@ÎMYtãoJ‰uØ'h|Ð\€{È©*I’$I’Ü€±@KëwºáŸæú[ô>?£@M>È ¦amàE‰ò%fö7LáÚ‡ÄRºæƒÐ'àe(?‚ò{9e%I’$IÒIò?½?'åoеy¥÷òוħ…œ~rÉO(¦XÅtªK¡ÔCyƒ8æQ§¸ÖËAÁü\(ˆé}ŽIk9jÆ ÿŽ˜Npw@ÑäÔ•$I’$IòAÁc)£2\yx=y½×prÐ×xà½ÎÉa“Ú‰éå>—˜ñÜ[“©’ýG¼`¢—g¸äŽNyA¹ Ê´õ5f°C›ù—xû%I’$IÒ'œ f#Q5eiS¼ä”p ¹eÿ2··Fæ )@ù5”IhŸ€ò~– îFÈläRt ”Jbƹ”ƒº1Íì‹$1–^’$I’$I ?ù0×ü!Ö|€„O®$réÝPî$¦êûǤ°˜ ºþ{œyøEýB(h«ðË),I’$IÈ;i”’¾ •ßôárîöMΆI`†gYí‡P¦BÁCÑžÄÇÃcNFÌS×PBïˆzü|('¦^’$I’$I ï<Ô¢zÉ䯽äܦý$¬Fq ½Â³©å†Nôˆ¸„Ž)e—t T³çÉ“F¹B$I’$Iyæ„R8F”ßr`9)Ž´ƒ”n4¥ñxÎûHçËeŽÇš~ ÊÏ¡D²¬ãÛP¾ŸkW´vâ 5J0—$I’$ äÙQPñ ‘_}h-ÑT‰PUëÛ‰Ï ¡Áý‰0à °!Š×pØÃ¨JÌó[ÏpAõúÝÄ<Šu_–wDç½ sÆHx«áfRÔòo»$I’$I’@ž•Bù/”Ÿäuk¼ñÀ*rÓÞŤD/©E#@:Gg5<Çû›P'æ©a:Ö–FýöÛ¼i ð.‚:)>¶™t?°‚”ÛB¼Fè3RWýð;“¤‹§¶¡Ý{u·FûÃ_ˆgŸ³©!¥qI’$IêüDc¹Ñ‚Áiu‚'}¿Æð­_ ÿ»,Œ¬í6<Ó¿‚l,éOŽ{K5âÓ#"€‰b†3ûÙÄ Uìhx~y‰í>xFùßr%}ëັ¨éRܰNì6À›2³}š¯üÆGÚ¡í¡@,; >+%_üF‡¢àÏ Û‹é5Y´æ%bÆ­wP"÷@ûþ[þE(ö1¥r¥H’Ô‰hûî]²$uJ Ç$'wø|´ð~¿yáäâ2òné©ä¿½G’ ¥ IWt;af5Lé:ˆ˜¡k˜ymö+¡d½˜¢¸n%¥Ç6‘âãÛHQó>öŒŸÇ©¤Ï"¯kÆ_uÍWB‚ÝÎ ­=†‘vx ˜Šß3#i¶ïßP>›EÓn&¦ !kB­jzí]`j$I’$\’ò@Ž©GWsiÔÈåaÚ²"nL³¤û òÌ€r² $]€ )9RCʵÁ®)YFmÔ¬/¼2ÅÓ˜>rkF½¦Z\%¾Öƒ }„ÏL»¸‹<±=îÐf¼.RÔƒ´•IN ˜jHïT7²Âv‡ò 1³ÂeB¨ÇGóÞŽyÙÁ•¤÷oK —$I¹$ äiù<(ŸO£Dãg¡ü/ÀææR-DùJÉßúN óKú’âcïO¤År5CýŽjk¯‹I¨¸ñÚ„QbH¾À$1 ¬S'‘PàÜîÄ×v˜tûh )9¶™¨áÂTo‡°- >Oœ8ÈCP‚²‹$K’@žo ÇXè9V×å¯Ðòû™Þbò]=DZ”UîTfík(ùjž û{Å’ Ÿ#(›@NSt’NÂÞ† Á íâ$ÇÎ`æ¡’þv0 e1ÉÜÛþ<Îd ähÛ°éïÄÓ~Â0!HêÔ4†˜Ÿ¾„4ÍGïÈ.’@.éãM'[ÜÂc@•ÅuÝ œ;•NA^Ý€Cáq•êÄP«µH¨Ä6Ó¦±O{ñ¶÷äMÝŒ¡j>hgßmÏ“CC/7íæLÇT²·p);¸y<FÏä"ì“¢¦ýÄjÒx×YÃn¹Ov$IÈóM˜Ìdh×-ƒò%€Óàf¦‘§X˜¨Fá­wx¡ýš&<*ŠÓhGN+vÕçLÍX:ÅT¬’Þ,¸o&¡¡¨ùCÒ{Ï›†dÎíýèø6“˜!wé&¿¹”˜á|Iähr ºÏ+H œ[aÐfŸ€çLjtfµŽ8¶¸l4)áë{71*–$IyŽ©?”Û³¸Ï ¿P¯!n>Ñ Ã9OäúÄôôN8_á`—B[Â’ üÓP^·$so{Q3<Ùï"¦º<Ɖy1“  îõlŸÛŒv¹Ö¼‘k M_'f·v½FÌ8|I]ƒ0]óg \BÌ“û n'ôÃÄt¦}“¯‡ý² %I Ï }‹/ÀL¥”âwª¢3ò:­Å¾OšB͇@Ù‘‡ç»J±é‰ÃÆÿ ñ„[ˆ¿éÒÔg¡šä ˜_È  Ó¡\@Ljz `ÌðÈ2 n¬““|þh"ØZÀy†¡|÷äÉrš$w BæÏ(ÀPÕi^ƒ'žÎ ®IL‡<Ê£P6É.•ôq “•º U`_Ëâº'¡ÌÅ>À¼õâ­æÂCd*JsðÉG¿~>‹ëâ$Æ@Ãv{(ªÉ_΂aJ›ÐŸ TÜDü=𸠩5 FeVçÚ$uÆ;éÙÝùéZbšÔ~˜ˆ»í=ès²œ˜'JGIÈ;°(3=É Õa?µÞ„óêäÒá¬cWäáùÐ6>±# S¿ù€‘óÜ&£j:“V."f¦»ô¹ êɧ£ÛõÄ] šúš\æ]šPËö;(ÿ$¦-_Qn¤|J¨Ž@~Ø)^ý¾q¥K—’ ÏC‰nxùëùš×h®]MLgCI]PÕö”ëRüØn"¦Ó:ZžÍËhþÙ7ˆi’Jf>9›KægÉn—$<=ú!ëÃ!rרèu€c9lü•œ?ÈazÔQ¹¨UÜþÆ]D1ÃÄuÙJbƈ§K˜c>}U?O€Ã”¼…!#ãtFžç™ŸÏ5I]“¾OÌh7B÷ù\¢FßÔTa&;\,!þºÊSÄôWAâ?IêC§WÌY"»^’òä„v©LËÖóˆÏ¨4~„0²åD­nÑ$ÎÁ炾ÓÒ@pξ÷t†ÒÍEiÿú6TÜŸÉš—PänœyÊ'Õ3¯€¤®Gœyw#<M& 2¨ó](×ÓùÓMåŽf¶ûe÷K’@žœ0*S'7T¯i""¡Ž¬!Þ[½£„üÁçr$~.WÂô¨xªq(KbªÔ—¹Ô‘.]”Éx£÷zžlä¡z=ŸÃ_–Ë»KΉßñµäD"æ ­YÖÿbúè„]¾ÿ.”j9 ’$»Óþþ#bËLæ(߆ª_=$&fÁþþ 15/¹ƒë²¹pûî]9™TçŽÕ'ÔÈ;¦k/“#Ç}^ß±"¿ÿÀêµkäêaÇŒ©´µµ9~§zTúþöí9cÖÇ9÷Ìp8<Ê)ð¶'”#§ÊG£ÇŒùàÙÿ+ÔábøÐ¡“µˆVîòõS0>ë“1ÊCÏ”©CÎ=t˜sRãcÒ˜W»Ú†}é%Õ,ÔÖõæ˜p”L‡ýá'#ËxÿàÁD8ïqS­Î#,Œ|°0Ž¸Û½œ;XnGZdÇ÷BSótž:`Ý—%×Ú†3óÑ(~œ©áìxŸ˜ hÒÚ÷3“£s6%ÖÓ«øVÛç¸q Gñ¹VƒéNš‡Ä 5LÈò¦( ݺsG‡@––Ï3Æ>åâ`Æh‹´ÝðÙn…{¾îøç¾·qãæÎ¼‹=fpkkëÕº®c¤æ[è.~¯iioo®\±bûÙƒ‡,£ ÌÂ$̲ûtÓ™›d[qIñŸR]ŸÅ|Ù0lÐà—`¼œ² ÖøýþÓ7lÙ¼/ãPÌ¥ûn¶=·(LûûF–]{%ž ÙŸÄÇ"£úbæŒï¡cßWˆy¸ÒPâlVD°Âtʯó¨élæ*úDý’×Ïlsá7PÖv@0Ät»ŠC¿?LÌ,€ÙÖ‰Ñ_âÂÏ —¹‡@¾ž G˜:ûƒÎ ‘Óœ4í ØOÊð÷hkJ2É¥·º†Ócvq–L@~ˆªnÃÃ8H¦ ägóî §ócÝ“~6ÜD“I µ=‰DnuøêË<öØSŸþÌ%9á np\™Šò4€ÑçHêt­Ñ´Ê)=:ôC¨û\JMExàGøý´Ö–Öøý«¤¤äþµëßÛÓ™üüššÒ½ì½ ÚyKÏ… 3~; 盎 }>ß½ßß²(Ýûýô'?QázÇä=”ÒåÀðä%/:Ì‹¿3âäÝÂá0:½ý3ÙœŸ>eŠÿàƒˆEu¯ñ­L%A«€ëô‹Äù<Šò \XI'wÆ)¼ è£OÁSüÚcÜ玛ãéœyŸ$š·W:ä8ï%î~"õç}¿0aÙ#Pþ—äÎü“'…°ÏtãÍ4¼+aÀ™½•éäpÇ$òù$ðº<ç²rTŸ&S«7s$ãä$KîkþxBDóœÔݸ¾šfuøÆwúSû w¸0qßI[šâ.eR3`1´÷Ʀ¦¦Ï 2ô»ïïØþ|gñ1#FŽokk{’dé3s úå5`R~@öh:×¼ôß‘ ì2¯ÞÎ׳öéÛgñ⦞cF2 7æ6U_ßN\o8“õ#j€‘¢{vïiqa>Ânõ¡ 7‰‰á&.wËf®Ó£ÿ|.É×§ynX!â|Ìn¤C§ f.êÅýWú É.§È)œÉÁþ¹¾ÐÒyGÔ‰”dvÒX¦çޝvjì »6Ò!2d6$ù5Í4)rdý’|ÿ‚Ñôl6EJ‰nqó\GÊÄøRðyÎàVÄbK·ÒáyÐ?œ¾ …B×åb2Ÿh8q…“ô÷}yÝÆ []6“¬€÷³V/‘ܤ÷íRál¾o’“LÈœˆ¿F:îøYLÊCðLiUl&.¾^¯wI¾žwÉòå À(¬wD ]A:/¹®UsÅ^€ðˆç?f âqS„˜¾cÈLJP¨{†KÕµÚVSÕÞ¿3JäéJ좴nWÅgrbÑGn¥vU0öm=nêh” Ð.’Ihו)¤ñçˆyÖxÚ”ÂÞý®çÕïomGÒK%Ñúž¬ÕÒ­[·Ù'NœÀÅRfÛ0¿0}Ê”{qSͶn®šý² <“Ëç>tèy¼ÿLÁðá˜l'fÎoÆ9öAÉ´2ÐþÇAƒdþד1>£ÏQÌÉ\’\•Ža;üšAbÅq<$9Žžé·ðLïÃ3%uc:;Ëmýø|¾½yEDJ߃g¹Ðá«hbx»¶¶¹+£0S÷À8ü ÅÏЇäC>WQÈ™îÜ|šP£ùlYYYUcccéú„þ©„ pêÃS7ÐG&çib†Àj…x€|…ŸQ‡¿™¨•÷ñ #r`|@îë?)Ô/™¨`-‰+ÓO̬R)ýÐËØâ Ó•Žý^“LŽáéeþBŽKÃñ†¹æ´Ù¿ €]mjõ»k÷[Ž‰Ø¯ukÖi{.|ö)èë?¸TÖ‘#GnMÑ7³GÓø ò*ÃóqùªH×´.{^9FR cè¶ÀxÝ Ùç0Ê`Ѳ¥'æêÿCŠàµÁeÍÞ4fä¨ÑIö}êõ¹9ìT}±¨‹ ïuù-¸˜œg1|VôWÀ×ïAù1SŽNÂ$d¿í¬Ù‹\C>"¹{æd(œ“l¦ÃKà|ø’8qMØ´qãM”Ú.r Á2˜“Aƒ] €4^¤ôlü·Â3ÍwÙô¿þÉxÌN¸1ôßìª@ÞÒ҂筟⸠*ÊO`¼LU0¤ïŠ‹‘IvÒúÛƒÁ›s$쥴ó‹{±Çãé(Ø#ƒ†L¹“Ó³Î|võàáU×»@Ó^Ï'È¥ï;B8|>†oÝ%Wv5Iáð5áܱgpÏV7Nÿí•õ«WìËseÒ-=OæÀâ†!?_yaS¸*›:;vq0À}^ÌUò•W_™*â;]¾o,[µ2í°œ—_{µ­[·n73V× øn¯™>#ïÙéзžëÇnŒ(lâßzcÁÛ-éÖ(üÐEC4ðÈáÃòløј#'ÀºÚe_zã¡GùMºu­®ÖÒ£.óôóÓ§LéQ ÇŠîÅÉTö\ÒO%ÝctËT—ïþ”BгÆÙÿÛå;´½«ùî˜Î ä­y{`E1&¦­ï£ÃŽëQ 0AÏO!AææfT«”&¹×¿ %9äé·y¡²²²çœ8|èók/<ïüŒ4°pSªW{ΜÆ~pÇhŸá2ÎϮ߼©>Ó:×¼·n/\û€Ë×g8pàÒ|żççNMò\Oá&žI}hR€uõº s|)‘Tjooÿ:qv€ Ý“iÞ†ÒÒRLnÓè$u>txr'ì‚dÒ=~þ-—ëp_Êæ<4+99¶¡é!ï§0vV ?’ìË}.*oŒcÎÔç$ùºHIy)™ùa¿~ýÞÊwGi:£r|'ÝùUËæðÕ ={öœŸI]?¼óÎq0NœuýM·Üœ³°¥P(t…Kßµùýd[o=žvÓ@ƒrM¾Ç"»Ý£Ýðÿ%Kfù%—ç™XhÛÿ'‘P“ã’O¥ñE¶l^•Óù\[ï²Vu¥þ;jô°$Ìës€%û²°×cŒÝŸ£ê¾2ßÏÔ)9¨ª’™Û6P'ðy1‡÷)ë^6ÿø±ã¨íå²ñ`r˜Ùn“&pE’ÍìeÑ$_ÔØÚ^D2s¨hëc¬Ð¿=ñÐèó2f<¾Dì\–“[.ÚÊCÛÜŒ• µ®Ï¶îUkê=xÈ«NÎ~0¿&Ož8©šgò17~õ/Ü·Æ­iÙ¦õz½[4MÓ…3vïÚ…Ž‚;%Üæ<€Ú£S]€ü¿Y¯Yr˜“3æé¹]©ÚÚÚpÎû]ö¥¹v†ÝÌã´<ú*¡S “ÃèØO¦DÎXo’»d$î,Ôš5Á†ÿf‰{&f s™ ˜î3¨þ]ˆ®jm£}*“Ъãaˆ/¼è¢¥ÄÙéí`’ÒJ4³ªºÑ_ëÞ£û¼\µõ•—^ÆXéá. Û›fj(u;ȦwɆ‘y›ÿu«°Ÿ¹<×¢lë 0êÄÉÓ¼8$’ò»}êÌMÕ­ûþŽh ÷¸ì“§^w͵¾.ÔEnò±O]žÎ’%ª{ Ý.¿œïꬡBÝ6Q´c_íòu¯Æ^öŒƒäø¹$Õn1bäÊB´?Ñ’Ì£éÉ-ÐéíìW‡üúÑž˜'Lš,~™ªŽ÷ïÇØó3@è ‹Ê³ÔÖŠ ^â°-ëðdÖ577·9Í{ؔѓa>Æ =Ø>8 Ø ÕÙŒ°L™}¦E4×,‡‘HäT µy'·èF›ÞçŽ5ìä¬AƒöoX¿Çå¬Bró I JÒ·÷¶´´ü,˵œ,T‘”o r;…± sàg™U ÇMê.‡ÂÞ®Ð7˜©¸§O=»­µuY–sž%™Û>‡:ÝÎ-É à;ªZÏG^nB2KçÚ!zgÑÂ& SJ²ÙMÃl^ñ’L¥q·‰)***ØáŒeæI)=ÜY7m8ÙìFlÞ¼iz²kk‡s8|µê·¿ûÝò7õ—Ï—”t8EåM7ߌ[«[˜\>Jœ’LQ@̈Œ’,Jq’} ËÆewB¿b;jÖ¶ß—dYJ“y·H$Ò%´†ã =ˆ{¤‘§s¾4 &¤ ù±SJÚ˜ªdÒÙvÚŽGL×O/äà‚—,ð¿{sssÔmA)Ôêuÿ÷Ûß®+6>ƒŸ·|žiañ<èÌA NšH!Øü¢ÓÆ‚Nn¹:5 0m÷û‹Â­Û V» #Ù‹|²‰uïÞå}}LèèÑ£Á¢zŒºÌdb,¯~W9¦´^IÚ¹¦L:§/:³PIȸñã1cc’Á÷ºwßG’x!ÌÎ8Ò˜oÿµ$òL²¡5 èUº¿3ÍÔ˯¼UY a-À,]2iÜxG{*Æš»$»8Ò«W¯yyhf±ÛBÓÍð¿œìÁIólj” ŸÓ·aý†|«m³¾/4°z9@Jr—ôó ðžNVt>”æoFN4 § 'ë<ûï…Î<ä9‡Q.@>ÃÍ0ÿp(ú|’ͨ¥¤¤ä¿…š{7b²32¸dëúÇnhéL3ü¿ü¥6ûßÿù;ôý$ÛWe˜5!W45âÀD=¿buÝ‘.ºØ‹:˜ 4‘üœÜäæ÷à–¯ç‡û÷—‘ÌÊTJs³™÷ù|mäãC>®¹¦¬=ícÐ?謗ýñX¾^8¯uJw’$Î5ö=Y…Qò-˜ŠºÈïŸÛÖÚŠ©*ì@èIÞëÛ0%¡û#ÒEk׿·§Pmnk3I~ ¥›;ãêáNo÷D§·ëú“Ÿ<†`oûü«N›lºù:þÓm!2EÉM.n·CzÚó¹*Ýîé÷û/èޣǎ\ßð¬³ÎjqY;nç+ô¶Ñ–(ý0ÄåóÒDºQ—>®íÛ¯_^’ ]}ÍÕ'ºPß8ö¦ŽîÓ·ïr}CUQô|?TÁ€&ÑجÒÏÚef z¦Pí[¿iㆡg ±.íÅ£ÏÙ{†»eáR²’ç—n¥:´)³1 õqea¨ØÙƒ‡<kؾ?ïù¹SÈ£ÚÆ3ÆÂ)g÷Šß<ðÀê<5Ñm“êÞÔØ„j÷…ôaÂâÞFi>¹y7&Añx<Ç—®X^°œp¿=š¦1‡MÖÓlG‡ÎMù¸/Oñ;ƥ﷣¶®ƒš‚ÒyçŸÚ°~½«ö¢cÚ æY0ɼtÕþ)\BJë2Zº>­þ ×Ô2YÊV)Á`ðWI.?Ô«W¯× ÕÖo<òš2Éqñû<«:ëDôz½sØøh8ŽËþÖÚÚz•詪ú·|ù&À†~ ‰´ØáCh–,ZŒÏãæàùQþ–$u;‡ÛýÞ¯ã_VV†Ú¢#.k¯:_÷ýñ]w¡–p¤Kÿ¤Ëºi6Š ½Ž0›!´ûK?öâLcÁˆ±¤¦¡‚›Æœ{.u“KÿtÙr¥¨¥ÁL¸Û¡M7W²3JKK1Gt£›ô¥* 0yݪ£…jk[{Ud™dSÚ1üÔ^Û;ëD¼é–›Wß;©®W=T›ºIÀe'c-%™3§¿õÆ›=ýP¡4œD3QðˆŒ??ý×0ôÏ.@~úw¾ýí.yþ|Á€¼ìÜÑ"™äX¦,þ\!;ƒ UjJªÐÿªçÜôT7±/f¸ –.àKÁÎ:Q’fèïNRoCCƒþ÷Þ{몤'ŒÈäÑ,¤Å-ÄÅ«ÜåÀ–Œ( M$Îf.`ò^Þ×’ÒnŒk²sò@¯¸|5dó¦Mçú~¸ióÜþN´ireåÚTu 8Õ´Ç:‹DÎÉmÎôijj^Ȇì·ÐÊ’“Ñ90ÏÞuùêì%‹ŸNº  ÈÇÿó™U”ŒÒY2Ɩϼ¨´ ’{û°¡ÃªnÄlsgf6yÉK}2öèÑãy'ÀÔ4í+æ|`_q¸L+**zºÒâZiñ"ž„#k‚:.sùjç©§–7‰¼~Ý»ûÜ6}hÓ§ }Røpô7€9pÇÃ>˜Óýê×^¿þŒsÙþƒÒ[ª:¸ ÝM—ub«€?€žY9IÁz@“€£y8þL!ǘà¶$ŒNGTÙY{ÉÃZv;±¸µµu¦òÔ=8?Ã+Î îÛI!›Ø»wï7H†vIXôó^~íÕ‚„©Tßõ¯"Mg·gxÙ¡Þee}2âI`.™Þ*‡z…ËI]KÖoÞ´6ÿS×UZ½dÉâšlëż×nN Ð ^{óÖ<3®¯»I' ºãIüT¦>úð#7æê^å&ôæÀ-Ÿÿ‰ÒÒÒ¿gЇû\˜¡èL—Mû<ˆ*ð¬TÏ5çÍDælƒK›®Â< …ÓÏ^v)î‹{]$ò¬Ïé†çÈÚl{<ù!†ñËÈS·WÏE$Ã0Êo.dQE‹öîLæ”Ïç›[¨öÕm;p1«O)}uïÓ·t‰øjèË¿8H"^X`‡ɪª$²6öˆ‹Z×ôd[o°-ø=â’¤'èΠ˦vW¡£üÿïÜÚ›÷ÿŽ1r|Gã r“˜aý?±æ½u{3¨ÒíÛ¡?¾ë®¬rå766ÞIR§öt$<(‰fqÐîÝ»¿Y¨ñä&³.@>‰ç>ϘB¡Ð$Kg9¾Ç»¥åžBCAMº]ȧ¯\|”*ÊÛ]ÄXÕ;ÃFTÝèìL‰oÞ|ÓêB´ë´¯ü¡Hã?Í\ê¢ÿè*ò7<€NoN¹Òû8|öaŸ¾} ’€7vL8ã²!‹ÿk™Ö9|èÐó]bâ‘ûÖ[åû¹0Éòã&¸Îþ÷~TÈñoãÆMОÇ]¾îÞÖÖö̳Ÿmýè0¹xÑ¢§aÌÜ4};úôíû (^¯›¶ UµWf“®ŸÌÅrhã#---« ·Û‚—L>†Ìß:ê' eÿ<ÃË64µò­ù®¹p‡ ü"<ÏgRHqÛºsÇ i JE0\’lÒÀä|cÛ®)‚€C ƒ‹YIÃQ@ û4lào¤ªoàõ8Þü¦ÎÚºú'ùéþ½KÇîúóMÉVýÿ6ôCŸÿúüúLê‚ñX ßLÒ÷  ï/H·¾‹/¸°xû¶m8CÚ÷h_ZªLÜä_›ÿêó)T{õPç T¡K}¾¢£”‚`¦ée¡Ph <2¤É6õM}ûö­N7œ6¸qmmmhŠð9̧‹Óe yèáÂsÍJ¦Bÿèû…^¯wU”x¶lè~x®îáp—&¢Ç»]s’îuïïØž‘Í6Ñ‹Øþ“ØÐ{y5´ Nmƒ¶5ó¡C[&`›Pñ6ó {Ä­ßýΙhDÉijjÂ\nÒ%žp÷:”¥˜1Þ·@Àß30ÔÏs ñ¾èÓ‚ÞðÃÚ÷ÿ`¾þ:]ðÀFsf2Uñ:>®«|>ß>˜·Ð6˜OŒyaÎö‡¶‹Ç9#¸ÙÚx°_ÿþ£ÓÑ`À~:öSÔ º1Impÿ× ¬ ˜Î›À¼2ûÇ_M±]»}‚†2“¹6|èÐ-¢½’b~,‡þyAÿ|ýÓýSÄûç4hç8(UPfØðbí5_¼v²=Åt>褿{ʺ=n8ñ}’™3Çè¶Ý{~ï)DÑîýðƒ½Ë7·_Øñsž¼äª±ý‡ý÷?—=dØt>üû·0ÓØ}§~ùñ'Ž5¿€~qpÜiüÏ…ñ\,˜g`áÝMk}ÿôÉh:M)¯øÚáÇ_†·n±ÖfÑe¯-b8ëij¿ö—_Ϙx7BК7wÞ7öíÝÛ;‰ÔÚóßÃßë`£šâ~s”à: õÊÈa3FúUp¯&HÜ8'óÍ>'u}˜š’—_|é†7¼Ö!k׿·˜ÏŸÁµ$š.fi¦U0ú62ÅP_‡â¾ßÛ¸q3€ÕµVÏ'ÙoÇÂ}Œ)Ä?!’^²©þGF_ƒgSJa[6¯Diîó=—Ÿày/Ç’Æ}ÃÀ¤ÞÌñíðû3;Ò?ÀèÖ» æØSIæ12àS²èŸ±óžŸ‹@ž÷ˆ!ådl†3Ö¬ØÜ|¦¶rô`¿£väø)|%Ý-YÊV ãeÝË2IÉšZäÎpø“ŸþµÆô½{pk‚]hÿß¾u°mÞ÷Ñ»[ @5bØÜþÞeÇI¥ß<ðÀÊ$jÁ½ýúõ{ùdµ ãÊ{öêy9pèïäpm ½NÖs¡J{À€_È0—BZ1ô˜â Þo´;¶¿.$<@‰·÷Ê]»vý59é^RòãÐwOvxSV”ßÀs?‡¸‘‹g°Zè÷ûQS¹-]>:ƒ~:/Ýß2änšy²Sÿ|žé-’£Óù ¯ÿŒ¦~>œãþQÂápu!æ«r²6 µ¸øA’ùa=|réÔ7Î2Ä%„T%½›h«=áTL¦—ñ°têÉ‹) ƒ)ÛEcaW?[öˆ þ(HÛFhüý6¼ÿSý(¡ ßÛ«Ô? …À߇ö=sËaÒE Þ`qýÙe!ÿ{Ѳ¥'õ´%ûáÇ_ÊŠ‚ܘfcPƒ8'»ß±_¾mBÇÊ\]¿$¨«®½î‹Yi‡ÖoÞT?pàÀ™Ð&ô&Ï…sã 7˜_(ýHRŸÝ±}û¿2ó«®¹úVâ,ÛÕ×þúßrÌ™6 %â^½zUAýxB`.|f‚]PVVvwº ïÑ Aƒ®Iâd–ŠŽÂ˜] ý󾝿,%6€ù‹Å%Å5Pç 9ªCÜîëÝ»÷“…X·'-¯lÕ{u;ß:êI²¿Ÿá¥#Û?:„™àyÚ³gÏ› $»¸¨¨(íóf×mܰuÒ¸ñ£5MK:úôé“v\öŒêª=K/A;лӄÆÈ©}^¯îZLgo˜ÿGï¸&x€9yÿ›¯:öÙ|ûCÂUîCô,¸Jž/Éç©6ž—>ŒÏǸeãä¦kö½›­5« f< ÐGŸ3bN(ºM×uT3vOóòVL ‹ýQØD^íOì*!°ì×7l–„¹ÿbccãð\WÃG™z8ùóýáÁ‡~©£ÚpÆíÇcGþK[[ÛMü8áAVƒ¡vÿõüyoãÆ÷ñƒ1#Fîƒúa˜ÌÿS1a☰(Õ Ð åGÇ] kúÒ<ð:yö¦ÿÀµPnk8«Ã¤xû¿óõ)˜¯·bVB’yÒô†Ÿmý30Xk2m7U|eø¡ó1[Ibº´Í¥—Å{Ñ1SdÈ:¢%²¯ûJtpÓùØešxf ÅÅÅEçØBí½'ÅÙ͢ŧö?Ž“!ã˜=ª(¿˜¹}ã=äcF¯}Vy¬þÙ'`pKiû÷æwv¶¶~δH$’ >õù|ûQªÊ¦NØt*9‹…PÒüþöí 2­›Ì™=»À-!«•¯¨h7HÃÎi>~̹ƒZ[[§£ãlÈû^DÌC#P :³€-0©Í…½jê´î}ôÑ 'íZ÷îÝW¤>©SµnÛºuŒÃx¶rî„ԛ싚a³=ÎÏ_ÌÖRèÓùšk3«ª»}øáþILgئ‰PúsP²úûûž\e¥×ë­i°Î)#€ùDŒOOÆ JxÛ®—f±&¦C¿]Äû ÛXÌçCêüÛë㔘í×N?a@Sccy×é÷oÆøÿŽöáıãNknn.çŽl£ ô¶1¢¨!<’/”MØÖ²îe‹1´-cxÝ5×úV¯®«‚1DßñPúr¦”rF½ÝÃ\šÇLÛ¯G'â`0˜âÕ­[·úÕï®í°6iÜè1ƒa^àüš eŒ¼÷Tíåý³ûõë·ìdh O*#T~3HåeÕxEù€ùÃu÷bzû›¿|B×YÊT”æ˜ßA$uZúìÅŸÀFéåÌŒ–®ãTW LóŽD¢{ÇàÁƒƒéä&Ï'a–0X;˜w‚úH&émyd ú] søúCÕ£~ mÍi†y>tȦKKKÃ…J뜶ñàƒÊ¼¹ó¢@¥(”Ýùƒ¶äëxàdë¥Oß¾ísæ>ßÞ™úýûöùwŒþðz(E¾=&Wlû—'âBufÌu'‚T~10¯¤ùsìTÁ×BG¼ZtÊ)õS—ל¦ýíÚÞÚæªz‡Á\W©rã–oÎOÐŒùóeÁHÏÁ¿¹ ~ó¼\’$I’Ô…|ömßòÚÁØ dÞ‹èd}‡@ÏHLÕÎiA*¼p.€ •‹õ¿Û½w˜ç2´ ôiµ´ä…ªwW}‹ w½njv~ëtÔ»(‹*ð™¥%íì{7-¬xöÚS޵ŸKYVqõ"iÔ_Á|ø“Ÿ¾™;Ö©Jýßôõ7¾¸\.I’$IêÂ@>€¼‚ƒ¬“š;€Sˆ3û‡ÔuAJŸÄ¥týÌäîP‡ï·ÜvïÌs­þEU÷; /¨%Å‹2õ¯pÓDíÅ®;uŽÖÒ†iK²i€î7ªwlü‹ÁHý󋽚ŽÍ#¤Ã`zo˜uÎÅÿ™½åÕûÄ,|×^ì LX÷µy[ä2$I’¤. ä{æÎV‰M"Oâ®ìÔYRÇÿ,)Ý’Ô'Zöt¸Æã‰¹SUæ·c¬xžúÏ~Þ ¾±V)*ÚûÁn#G4Yvìm¯Í±ø¢po\²Z9±`y7­¹µ—ÞÖ>œèú~LŸ,ÚÐäé^6ºjíJÌSMv.˜C¿²{ö)…›æAMéàó¡7ýj(3lŸÔÓ_6aÕWf”Ë@’$I’º8g"‰»¾wAwf{S'ºÆ%txÿöû®aš†§žuËsŸ‡[ÀÍ͈ìçÞâÁ½ˆyx„PÔ±Q  †þñ¾ -ËìpÝ.Ì_‡ÏÇæá9·Žï7bÜìÏÿ¾].I’$IêÂ@þ;½fn¿¡©¥u&¤8霙ÑÎãI^Ï–ïÿ¼’…Ãxžî°Õ (Ê]Ãþpïâu‹¹)À¼bF~ÅûÏ nT"/B“Ó{²ìÍ7VY “]P5‹É%!I’$I] Cör OewýœºKéPS&€PWsŠCU;EÉ\M®æßú³ßŸhzçŠÉèŠÏ;¹ášëD†Éò+(¯0ßùì -ø"ÉüLèd@>çíQ7^Kíuëí32÷]‹æPš„Ñ;s†d$I’$)§@¾/ ‰<ˆÛ_RÞD¨FGx…%JèQIÅâP]q^îUSß{Óí÷}‡ižGÞ£‹Á¦Ð¨3*νíëÁ($©áÒù»Ÿ `þß\9%ôÁwF}íN'­KÂ81gPß¾pUDÇ?1™M®¡±ê5æŠÊ¯¥1ð§NŒ‹yFEæLÁî5sèY$3!I’¤ïwr;8'ËÑ*ÚÉ#À+’¸˜× !k“fÖè>K{„7}ÿ£õpø×PÁÅ]wè›®›ù]Cúža†ý‰™ò¢`^±ÇÌ_"9P³«T¹ó­7<¨$Ë´ÏâA7¤ ‚»€'dt™3ÖsÕ-3ó MšV£–ȸ J“·Ñõ«yTå'Ù˜7:}œwI’$uq O&•S›äí´ãiVÒp•¹ÿÎâQ0'‚ª..¯ÉÜæb;"ðà 3¥ø}ß»ïË ÿ^žÝåÀã¹ììÇîžo½G¹èŠÐ7˜ƒäúù]Ï9¡_è¨dîaôK¿:6xö¤éÀp)î@‰GtÄ5fzí+vමâqfþÁꥵŠx ³Ï1Ïu0µÐX4„}þÀàõDhg_a{Þ¨Äb $°K’$©«àz­«&F$Æ‘Sç}O´y£ôdåUYr©'%˜sÉÜKÑ£ÝëI܈ñ·-º)ý+¼íûî¸Gäð±[˜®ßoOë"ýÄקרÁ¿¸ý¨Ø/«› Í$t…?£.áçvÿãìF­Áÿ¬loìgÊ…÷T«siXQ<¬›Œ„BÝ%q7g‚¾%pêÌœ‰¨9ijÎXlpÝ€xõr!à WÓàÎxœ!Plœ@ª€ð?ÏÇ\úÎ÷ÌóšÈ`Ä­5Ñ?a”d&$I’”ïæ¹‡â"Á$9W£¯D)¼ªF÷8Hrö ÒÍk—ÈVÕš$s±Î6”ÆusSTù~oÄî»è§hºýx;¨s÷>7ò‰ûfÅ™ eÖY€ŽR3xÝâ ¹`ÛÓ3ÃL{U¶L¨½›®–ß×pÖKÈ¥Ø(X#ƒ¦A_óÀ;»n·‡;©Ôˆj8!6Ðç÷@Àµ€˜ÚTù:gÑ ’ª$Q=.œÚC­DËyD‹Í«IüÙ¨â̤Ú'¤¥Ú·êÑ¡!gŒÏT·¿Ë}[¿Ñľrö`±g9€}Ǧ˜é A#bËÐtÆ0ÉH’ô‰rÂÁÐsš: Í’ÂQ•^ ®&ù}2iœ8€¸% aÝØ(ü¬b¦é‡m Á­üC`£ÎùáwßÿH÷ðácW *œÊÞ¹:_U¿=ò÷¹F§BûêVpç=ÀàKcŒ(•Û}Ä3 ¦dUmjå°ê+¯àZ êÌØˆcÃIä|äK—$©Ëù.›³næ>kSwÆÛ¨iǶ@<ÁÉ)‰„áX"؀㵃¤~±¹9Z\˜_ˆ^ñÆÆÎÄ’²’yàïýã³E-ïï˜À4ý|±0e騢5"ÿ;ëºßfšv<è•ñ ¢uý.­™ÞÿÒ™šÕ@Ɯ͊™;ÿúé÷‘ô]œ%ÿwÚ%3Ç`õÜn-†‘!ãÑm¸wf”b: Z`'^c™>&sWhjmÈÈX5Ë’y=H¨‚To—Lí†Ú Ęå,ç! ^íö¶kº â jqS»ë$öŒÄ¦°wÛÄJºjLŠ·žµn•ÉtDç³ÅÁëI\;ÂâUî†t®Æ®;½ÈÞë¬Ï¸\MÔà‹Ó¹ûŽ­ÂyгzߺXã%êÄhÓDX4ìtÉHH’”3 Ç¿^,Þ­(‰/2AÜClŽO‚<•3‘õËKpËÎ"S*7?@×øNG© íè„oJŠÃjÿ«ñë·ßý»>‘ÆæQ€’#™®!èõÎX?øÛ›˜iX})唣ИÊu7ü]ÇTºÙã÷¿wÚW}P2l±/nýÁ¯Fh­m«àe@ò_ŽûÓ}w'«ÜÚà/Ûý¯G"­ÁÛËr2è„<÷Æð¯±<ãÌ-p5Ì&º m``—À‰²‚¤(v8¸Ê™CÆÐâÎ,&BUó,^½­ `° ÚZÎó%µYl'ªÜ-»»ꆆ€3(Q“ø¤CZ¬½v Ffc·Y»©»Å1µÔôcAE‰]¸Jý†dŒË¤É&˜‹Ï‰ŒÖƒf‘™µ$ùèE.ºû å1ÿ¸¹§ÇÚ‰ ¾uóª‡Q'íB ëj;cU÷2òP…aW¥ãÞ®Åêb4ÞaÑ^¯µŒ;C2’>á@n׸ù8˜[„ ¹—#ˆƒ$ì166çdFmÇž1Æ\A\çjt=‰zUÜÌÖ,^¤„¡¾F’KsF¾á0ÉÿSiÌœGuEÛõÁC ´:Ò“…#ÝX$âg:CP÷ÇUEéa-CÄ£6êeÅ m5㚬 Fƒ]§bzµ©­°@ 6Ùõ7ßó ¼¹ÕÚï¯gæØ?ܽ4qD/Üùá-zørèSL~s ôÂ@øÛ¾C¦sÏg"¥ÃX=úæð¯ÝnuT*·¤qoËuJÐb®¨Š~.C9—ÂU†Næ‹™P•(XAx"Úì9“}¥9KâÔ®FÕç(™Ó˜ëT<1¦Àb"–ÃýBanrPA‡Î€Ô)O”N…>ª¨äÞø¼ŸëVr°µ@\5CIò„Lê'ÇÔéÖ³)žx-ŠE(©‹6u±o,Câ·…!†BðŒ‘Øïê â:¯§n5÷‰Pl 5ìBc‘ µ1ôB½È×$ÓøY…±9ðï45bŸN;S2 ’º[‚•ŸKQâË™I^¼ÆwÔr¼Ð;  ¯)÷Æ ¥fXzSýÇâÀ9 sQƒ3;pðU½Ìˆ1g®››¸A0.¥[8h‚ãâF–Æ6˜º%µ¢¦3®®å=m4=¤«uËy ’æû?þÍ©áMk ¶BÈžâ3Ž=çî››mx‡âbìu\úVxñóy·ÞWr¸˜õlÕÃý¡o `‡þÇлðÿöefæ;,eVõ*U~öÆÙ7üJ”²×pÇ4QwʲFi¼„g?J®¢‰Ãò¶÷(±ŽVi ¤]Ué,–ÇpZãží–W»õü:WO«^—0Éjvë¡,³Åä1É?o š÷X³Šk+l@ŽÈÑM‚‹9³¨qˆèñ nÚÑ)©ÔilºÁŒT˜mzø+ΦCJWœ9`]¨¯×`<[ÛbÏæ$‰[@» ¤oñ{»™%qü 2µiœ Æ‘«†DÍϪznRc±1²€~â$S²g.Q â\{ZzÀ»Íöü‚d“4)4lª  xbƒ % )ß@n8믟Ki‹1ĬÆT§«„ åeæƒ]¹Hõ˜ ÒSÄa¸Ãi:H+6<u®3Ã…râ8 §‰m¥xt²³ÿ¤*sƒ°ÛØ­BÕD5±Ûº´T¯«›au«¹Þ²í {½±)‡¹v¢kÒ´*w»çµ%Õmøö½?…þ¹>xzâS÷}=Õà0ÀSÎTB×5íDËG£êjüÏbR®Þþ\ÏF­½gP÷Ô )ž  _:ØWÞµÛ†1QKXK”¦ ïtî¤Æ;£~Y ˆ°"nö”Of†•YjBè—W÷Œ·3.Œ›PV,3í½ 1{8V&Jåö1¥©û5 æü=Ú®-¦ &i{Ø3´·~U,þݰˆß?v G÷ªñmþ]¹Â¦rÆßZNs–š\g@“%²!œ)°ÇFõ9Ï÷èïW kˆ&2QØç­­¦Ú^âð© âÆ1_¹ÊpÅæÈ'š'¼šèCcíah蜅v[íÕILƒP`ŽŒ€¿îãÝz©;úÂ-¦¹ÀJ$F0š¨eqb>± ‹ëÌ¹Š¯§ÀójÄ`BJ¿Iùr'@_ÅmÔ•ÕÕ\EJ‰fÀã!^/ì|E°kø|æ®gx$Ì´‡ ½\$‰U3À<„ªñ4@Ü’òÄMRü]ˆ ží,1¦ŒÙÔè–:Ž ö=·½ž ŒD4žØÖ6ô÷Xš äYà×Sªgê>bŠLp(Šc& ìúÅÃ=ÚÙ@Uõ§Ÿ¼çétAÜRŸ¶qi ½–­7ú¥ •É3gê~Ÿb€(þÃ>Gi§-ˆNm·n‰\¢á^¸á/57P”Ví‘ LHÈÖLmDz²Ãý)‹e¥³$)Õ‚(À9¤ðÅ>]É=Í g6AeouïæLšáÉî2¨nY GÔB(±ú 0‡©ÜÜnJ¤8_TΈԯŠyákœq¡6›¶]ÇöË‚™6_\ìÐæí±Ù‡-)—¨ñoŽÏCm`nòÐFhŸ§Èå:ª¯¯y?P#¦¶ÿZë¥ÂRãóq_¶Âì71ÄNÔJà'—séž8øVy4~_l÷rDf¼ŸbOtË,{ ˜€_¯¢i£@Ÿ¡Í{ÕZx?bcD*Ëk¢|uà“Î#g¬¾ZRsœ$63ŠÕ—“'™æ1ZBœî‹ù³à‡Óá·–#çeÜ%åÈÅEáñÜ3¸3™!’¸Jº#€ü„”›ß†CXVM- ÖKß$aôVØõB‚}Û5ÑŒ[ÎnÛïBÌæ,C’'ša‚#–"H„N\¼ Ší³e08(9VWñMÇ[µð=‚Ù–ïÜ7K øëÇ?t×Τ@Îbýû¦£UìISH,4­¨ÈK¦wžð«ÀKi$åX0sÆ"ɹ(6P·úNMf´¤3x¡ â†ÝaÍ:øÅÚüQÊõ8œ?ߦ›áb†ã”Xh£è¥m\¯ cH¢T.Ž©kzVu\}o9x ûªêµž«Ù€Ü¯)œáá7S9óÓê9†ªtŸðÞþœšn‚9ÎÑìCmàÇØèf^; ÛRäžÜÉŸÕ«M¦ ¯±´1šÃ³QÁ¦.‚zœ³Iq]N¯0} ð÷‹`¼ &ƒ: quð‡˜f:ÿø2³ÀwÌ¡–!ºâøÆ„õ/\µP1ç%U†æB1^Û7ºYC/3^Ì©ÓÔ5Ræ8af;_2]È Ï•n©Á »'”©Õ|Ã!¦ ÙÃ¥ñ¡ÝJ éÙ@¼HîÅ•«¦áˆ6-$ØÔL½wpj…¸p"ÛÔ‹pž÷<îq`j³+[jssUISËnZ¸·&´ÍËÙ€XlƒµéÞ’€7¹Ãž¥F´Té8ª ĉQÙùÞ*‹§~æb›ZÈ¡æPˆp';ð„¦™ÒÔ®Oèf‹çà†‡âx”ØoƒÐ¡«–››ß䩱¨»vÃJÌâQcãjxl{„ìÄ9¼S³#ü¾V#‘XÈLX›¥Å„ ‡øo*á{”„+8€+êlœ³kë%qŸšØfåh"U#AÊÕ9Sai¦t>Ñ¢ªvÔxc!f¢}×Ñ*Θà3ë‚z\Uýâ$oÛ<ÄqSìN(V[¹&mE)ÍO®0µ¢ÒÁbb,0GNÎ^Åd2, úËW›Qº }‹péœmµñ5ÌÍï$2F–©ÁnçuùDÓÿ&NýÎ%õ¨S\¹™;@ÔX’¯9çTê2·l@ni "\»biWSzæsà]½Ê”Ü&”Wëbz[Ã'!hÖm…—­PA gDˆ4 1i[cBjdÕôHGóxã¹Lc $]¾ÒdtPЛÊ%qêÐNqò(@1~Â!âQ’˜Áàw+깚 fom'äÜqÀpÕs¯ò&!Ä_ë‚ä^çËëMÓJá>Oêm4Î8èy¬5‡f«±ß4%¶–M0·„€1m SέÑ,[d:æqÉ\·XU#(—àäjÝt’Ô ›8Ó@ãE£Òš9ƒ5˜\zÈÄÚUxKŧ ¸êRS9ÝØ,ljVKÊ?©]±D‰&•c=-ÎŒqÛ‰.:)°˜‡!å! 8ñ#ïÖëäšæÆ Ï9ë’sR"ãs/l¡–c ë5å}uù¬A¬P ntûwOês±è6 w’œMÛ”éìæ…U\êó‘"ËÙ­¬ÌtvSÌìÄ{I ì>%I}v)Ç¦Þ ñÐ.šäz7Ô“nUT‡yxôœ£Jm˜n€•…¬8h°ôì ¥Ýü<¿gÄß4ÌÛ¡Ô¢¤9ÍŒÙwRñÆi„Øên=JÈ‚ù¯)!5q:·ñÒØqž"Ûmç©$r«n§Ø‰fûq  *v\£PÖÁfï1ÍdZXR¿'§°0—Ì1;,¦!xa#5Ó•“R[§ˆj *´‹ n²D5Uú¢+¼}MM¹ni÷Ü´”x³ÏÎ:û¤j\Ü €­XjÄ) `)°«ttC@/2]dìþ€4ÀFŠ Pâçž^F5–;n„Yñ…è”G›f ^O8ÁJÐ21ÁÉÎã‰W±§-•»ØÁ5aSR€¼¬g)é>¤¿¬l7 ymí$Ò"Gú!•ûX’‹Æ{m‹c„ÿÊE±£<­öáÙæT‰9úQ›N졪eñ¹©-M.s‘VD_+=§ámĵ(¢¤ªz&,Q=Níë6°v©û%_,gŒëKH)Ìà ôç‰V±0hÕbQ[­=›Ó³ŠÖb· »fTª3“-íA­‹ª°D­†e7Wâ“Y>Ö³¡à2ñ TëÜVîMäN‡¸ŽÖ¿»X¡Ð¹@› ¸áC›ZÚó uwŒ&!ñêü¸¤@ð×Ç=äÛLUº‘åÚ?¥Ât´’3Q''SÓ˜Å9hqG7äàʨà ó sÁïÚgôXN|KÐÔ[¸ê‰9ŽcM¥ ââ`£Þþº)à¶Û6Oñ9‘ZZg2V[Ú¡cÔ…;@Õ6Jº~šL°óð,4µ±po|Þ%ïÔ*—|î}sëeì¹u ?ÔDZZ#†Ê<‚¹ "îÚ»Ž£yºgœÖ;K(.vSC©š Q ‹…»áuM ‰­©Ø ͲBøÈ%y߈Σ¦ô¬’­[–+Á`øEŒžP¥‡`Pí”=oB{êrö<5M­3I ±@ûTÎäpfGñ:¯ý$;ã½f:ía¢Æ4o|¸±­dZV¢ö¥ãVf´!zHŽM[d½5V^ð¾ @ÜãM®õÓ¹._Ã\¢Ä¾@†¡ ‘Qg]n—Æïno_jyÜs­KŤ™úÂÕ UÑÍ5Ònjg ÔŽYÚ=¢Äqõ†t¦òjcÇe ›$ÀƒŠR:Û¾ºÂ/±ÇÇÕ._fº8‹qPäFbNÌ)WýϘ¢G™ ±þ¨ƒï8dt%¦Š€R»l™iC£Ú.²yT‰!Ô°mqÉ>À•¨Ý¹ºz²nJŒ4ªê¢BŸÕq‹jW)3gLÖ/X¥$N Ó´1³f’é—ý þ¿«qˆˆÐLãêZR»\±ï3€a@ êbଙ5ž%ØÈEiLƒÅözÉ’eËx‘70<­•¨½\…7 tyÅ´¬­‘pŒÇà3mÀø‰u°5êø°`}øåïÐñ=~®¢mïn^-L¦WVTõ+Œ¹w*0×iüf*&k°œ’ÒJXn“@E‡7d¬˜PÊb¹¤qá–´¯¸]Á¤9c'”ÊÀŠ=rŒ,›?_Y>ÅqÑNâØæ$ å™·Bz¼t)&}1Åj@Zò$°`l4|“0}$ÄCOœDCÆÁs­ªç¥<^›;1ƒ ³H1@Ÿj+Fó¦ÇIå°©n®[¢ô¬bpc‡ÕÙ±ÒØÔf€—ý ê }°§XµŸÖE“1`1ë êìpg¬'–~!ª1âLª¶ µ¼-ôUã4Ì> í6g‘!ƒÎü`ßZ% “†† :rºÎÔÄÓÜÜ I0·8ô&¼Y½–'ØAÇ._¿—Äy¢Gs#¨‰‡´[¿G5=<3ŽLŸ ‘Gã²¢9çÍé\<‹¨%‘ÇìÅ€eDóÚˆ˜[öªf˜Ä!ñÔ1É>HÚÓ§ê µÙ&ª’¨^=Ñfö \·ÙNCõŽƒöXÃ÷íEÜY3(¢`y5b}!ó–d±öÍ4lë6(¥&›Úc›L _a&ƒÂñà!zÌ€g=¡?k @ŵˌkªjÆsJ>[¡ˆ'“ÐèÝ—ìc¯ãqÍÜÔ@'dZÍ=êµnú$ ÖÂC–¬X©D`wÇ÷¸sh‘ˆy@ ¼7ŠÍ›CN Uía€I7î¦& wþâz#üÑFÜVõä „@8}Ш†K:DÝÖf³3[R¹›´ïäHg9oFŒLS˜IèCªÆú&•† &Lרq@†Wö·[ÃØñäh'ßwD@ŠÄd.íP‹WÉj¶•‘Ô¦€hòQý&|æ&0´F*ák“1gœŽON‚áˆ|Ë솺]!8j$NÃu8ûØ'H«„%7kè zvä=7(ã.¾D7bŽŽ6’Ɔr¼9×4 €RˆÃ$Éùé.¡cª×EÌï‡Ï‰ u¾i£$¾Õé“Í0<Õ#8T±Ä”J3õ±Åð1Û<¼ñ£Þ{uk+{w'axãÐm~g0w’ Å~Áy´Úì÷›{9æBxã>-»µÆ³z|BXI ß³lÚÖ¾Œ±ñ*fêŸ]c†S£}d Ÿ‰U •c¡ ™ˆÉ¦ =L¢ª•­Øv4ÿêJ¢–}€"KW.TΟV­{mç¸M +ñ‡:H‹ë*P9±ZG3¤ä%¦rÍZ@^3˜c”ÓùYácðP3§M× >)Ãð½Æ”øüôq޳:YT_«TM4-â±ò$LzÌ…87ÚÐâãaj4.ÏmMÇAäŠw–‰"%‹—ÎÛ}æ~Á½k—ã3Zê~TÅ£tnØÏõ÷Ý8#ñê &(÷ί®®„¥¤Æ§Âtäæ«v¥¢DL0× x¶ëCñÓ°ºænŠ*Ñ-q™:¯-°6]wÄLüÜ*„ˆ•ÁëM¯¼¨.…ÎѼ¹FžpÑ4Í(øÎl—(? Åt0ÑÍ‚»1ÀÇ-£S³8+AìTj9Ö­»«}á ïFÃR{Åôézq(h>&€9¨îœ$\ORBœÊ(Wï3f…„AÔZºl©¿jìZ°-ÓBt!Öà²0U­b¸èÓ•#‰Êq¢h"-GŽ“­AØdÀëac›hx:sÿTìªÏVDL`ò›ƒŒMÕHX¼ŠÕ €tKy©SW!;ÑÑÎ’Z,ࣉ猳d¶[Ašöùhì°œC<«ÏcK-êp:œ.Œs{„’¶½«Lª:O'ýŠ û¨<Ôb¨ÖÒ©ºÍQ1<ÏåW»†ÀQ½Îb e%«q’è‘B÷üÛŠ@±Ì<®ÔCc™‘ÝÍ’ôá³Sú ;ªiæybâþ™ ê‘W_ž¯XíC•uôÅY[‘B,^…Ý<ðPp(ÑN­¬Ö½ŠÃ|¾Xf]TÙsM’ÂÕDküáj`+«>e€m8‰>78Ú´-K•"0+ê„戩)Â=›…)™ å1|F˜‘ŸÞè?Í 'C|°@ýÞZºÐسϫä‘â‘®bÌ9#  s§kWm´)š¨u !ã«õ`5s"PÁóÛÑ4¡NaK”¨'?\7sÚ ]ñ²Äk1Ó¨ÀEݼ­ã¡ÂŸ™ŽÆ¤rÉnåRÌ#$ŒÚÎO}F¯ö)3tâÕmN7„°j‚·±8tөΣŷÖ2 Á½ÞªWÐAÂPË£Š}Ñ*È-0·œPot¶&œÁ«Çs¶D‰1·4–Œ zF¥Á¼Phµò#S.#[šeÞ¡Æž‡BYÍyu Kx@ÂÁK Q ÷2xÌô€µUÕTêvÈfqRºEÈ%ÀDPC2×¢¶ùhmßó3/uÐÍᆊ ‚j9Ì©•Êm:"ô´Fµ:~¯œD7g§ b&¡GÓOE™êîn;€¤%ýVän;H¼¦:?чB3·bHÔÔkÐZœÖ ë€uv; Ù†Åj2<Œ÷ƒ"p¥:aœù: 1;j; @º÷(%è>¬·¶‘†–6r¢-D‚èÁ®é\µŒƒÅó1“#3LP¯O?k'5S+`ÙèÆg†Ã˜¥Ö'1ßÕr4ãÓÃÃU4š!MQCíŒqÏÄ ó¡ñãLj{ 9-ô¸hEÜ€¼ãê‹ã¹5Ý|nÆÍnÅ<B<)I4c`¨"úîÙP§`(ÙÄéÕz3ô÷ñÆöX?§óÂmltsv’TíÌÁKœÄ@W·œ¬, ”:kyaAP[Äãé1ÞÜ#ØÎužiͲíö:½Œx0ƒíÞáˆÎ¥óijÇãS¸RÃñÏÈù.Ø•Ë+ͤ%V c"0g"¸ap\²Êrh‹˜éO±ß<°~jj0$Mw6?°øDOÄÔÖèIO5BS¹„n0 E1Q'®1íæŠ+Z5¹0]˜l„ ·}£}¼j²n}±pÑ ¥ºªR_¸x¥R=½R§C©âz(áCÎ\é*qŒ~‡²®OÙE©kçñ5ÜÿfÊ÷ËÇÆ™yõ¤Â€ju½‰Œ·IµÎ­4‡…i0ªKžI&—j¿xé<3ð{ê5ᤙ±9Ž¡>9?|«Îñ€Ö^ƒ{†4.âçOKRX¤H|+%H—¾NÙMQG÷å¨|«ò".ìûß–"† Ñ(µ›y£òTîÌã5½y8mÐ8(józ‹¹Ðº¤Ò&*ŽJ± ÀFo Ï•Ñk§ôE‡ [x2g‹ÀDî:U…À@©þÌuÎr:ï°bXƒT/''^n{qÆB'­e)’w‡ŠÚÏÊ$<ûö§Ïž—Ü›èÒD^IãXVºÝNR,JL÷¼.««` ´Ü*g™ìäÌ=~%âªN<B?§T{b¸,9oQ~7Ðð`V­à -Íý$}9Nº”üÿö?ÿOA•Q¯ÉÑh„ˆ7Q!FãífCÆh]¥uPôÕl4d<æEqkÞC9”¶µ;Nµ ñÀz¹Ó>(UiWŒ´DOPˆij4kžyŽ–€Æ¤fÊF#›êw¢bLŠ]pí£¨F*`ʶxþ-VW¸Öq*§PS¾º>iÉBˆJú^Ùµà&R¼†z»‹Ý&PÏ7}%ô•÷£à÷e-t!{+6ÂÜ3zp,-¬q7Â)ùC>òÄ ‚ˆJšÇb?[CÒyyƒ:x³Ý7%ÃÈÜ•N0ÒxõˆÕi‘²Ô†BN‘GˆFaÇã‹ ×"øÇ_ž“½Qj[¬WÕ6ÕåKGD£È™˜z’Y_ƒYC‘@]Dg0– "|¼Ž Ü ª‡;j˜ ³TS§‘É(MN¢ÛÄox€Bhù¨Èºq檟f°óÊK'T.z³U³«ÛeEªÝ¥Ë3ܰ‰¹ØÓf.ÙVgß{SŽ{ ;ú·ÃÏPqf $¿¾á›§L:S^èÈOÐuÕÕf±QŽmH¢l=‹t)“-®4WZ$ÒbŒJÖÙßàc®’^{æ»\—w5‡ÓïÉ.³›JÈ×iU»T]f£yÒÅûäác ¢À]uäe=É,¦]Õ·Ho`D|ö×2O³³Ü˜¶8gÑ6œØ¼éø§‰¤z¢mÙÚI:“èð%..ÕÏu…ù¡óÏ9yòyålaÕ‰ÿSL²Âÿ˜"nš·»ÑeΜßóÿø_ÿmm„¹QCçE,‡E>¹&ÞiµdBNœ”‰Š…e—«\–ƒ Ch×BŽÌÍ)hê¦ï”ÞW.3 &?ëAl“j[†~^.Çèt‰K rmF.¯ªl³8­±ù}¼VÏFäRµÚFÙ6’A«LÌ V3àtZf‰q¬`]“(¡±‰²‚J6ç&·¨¿Š’E[n Õ§i¶¹nn—çéêkŃêæU4¡J“îdI^NØ”§ø­\˜° lOÚ ãAý!}9;¡­ã×%pbœ¬¬Èj­¡B TÚŒAµFÔ0°Ü¡u$)rÜLžD£ C?r䨪y1þÝ8ý@ªÊ–Š7à†¹LÎöÖ6kB{…‰kžÅÖÄ~@óîù·e¦q?àu4j^z帢kð<Å©p2àž—òõŽFæ!“íÜÞ]œºƒdFåq–æAHeÙµ¨ZŸ~ ‚a)NlìÀ¯›œ.6w7”ËïÖ'˜éº*RC¶cl>§Sû=¬Ñ<í¢‹—Þ•ƒaO>ýuh!ˆÑtàb˳±µCç}Q3©ç_œ5ét" 5}ãRù{Z#U©¾Yê;¥g˜¡ã%u'•U»QÜæ!þÂÛïcäß3‘ª+§~î¤"ÀÊ$5*§ÏŸ–Ü9ÄûÔÎu\I_çg« är">‘ÿˆÑž;OKÈKË$ôØÎÏòuÎ?óþ¯¤ÆstìÙ“*¥¨ØYaljÿô9\'ÜŸmÅI2TR%9ýËw¥!%H8ñí×Á󞉲'— Žå©/ºØ¸V­åÙ?§ÏýZBÈ1>4 x'£÷½¢­¬”spä%Ö ®Fæ‰_r¦†³ôæÙ¸ë€KünH„öµå±ZšŸ8ùœrÁªÚ—[8p½EºH—¢|ϦÕÝ:¿MÎüÿ·ÿKÍ9p÷'Ž"ÞpžMi{>9q¯Â^ëu9Šc%Ë5lQÎö“À€â1¦7tê<5^&µm"OÚ‹6¯1=°Í0€ÙFvÓ€[0ŽÆ°Ž×9@ƒ?¢º²R›RÅ¡% VJáo Ì!iØÆP¥#ÑÆ¹*]‰ÂœSp­@ì\¥´NV”ƒÊ–]RÈóˆìæßZoÙ#^ÝtEqÖ}†¹ ·þò÷C1AWU6%­µ®¦1]}£±jJ`$é‰(_[€#e1^Ê€< è\Á;çij3žØ m4xÑÚVú#.5ÄöÚtþûz‹ûØJŒ†J)®‚<¢!­åÆNÙŒ—ýÅt/™‡2eÇzüµÃìTéßj~ JoΘà\² £›9J‡ŒH‘™Ç%~ß|†V®=J°“³äM—ƒÁRÊu(Ûù Rða„à5„gžÛ…ÁI ½»ë°Ö¸qâøKÊÇë |óÞx<˜4¦²Íéd¹Å !ªü…2[ž;xþ‰ÿòëHÉ­Ö¨²î¡Tïî£?ýÖ)IÆŸœþäulê®+•c>ýômI÷G<Ž‘k›×üS„Æ­€Œãü#qiÉoÔæÈ"ÐËd™'¡KNÓEiv¤:oyõ°ghSâAböŸª<Ó n¾hBZ–ÊM 0¥¦¦˜N¼öº’” õ¦…"SˆW—³ö\ŠÎYmb¹j«Ÿ‰µ¿ÜçmQÉÙß½Å$îמ9®Ò¨V¼›ëê|•g -Tãxpškظ†¯V¦°IÇ—ÅøCÓF¶EŠKóþ§2¸éïÖ<žÐ¶¥•f(µýöûÜ©OT¢KÚ¨ÃÉ£¯`$.6µÆ‰-ú¦´¨2yòHožfVKtègO¿'Ë"4,Š#ó,F»C¾éºÊS´SâMŸéDe<ëº%îì)þÿƒŸýC¿Ùí®6»Ó×öÇa£u¶Ölü‡(ŽGJÛ¨ÀFªZJÛ—¨™¹§¼·z¼ô±Í:øq¤Ø©#ð1"JÐÙŽ1jMTZÜŒ¨ª»UÓM:^aj¦)¬ †puu ¶Owa®Q‡éf (¥¥Sˆ´Ñ*Ï#Á‰S<½IÅf«@Ð ÚÀ§Ùª…öðÝî̆[º$±"-;½J ×Ap)-LÒ™¢n&ÅÑAGð‹ÊÞÕÉðÍõ½’3Ã`g®œ³´åŸiV'FÆYå¼ÚpÚ˜˜MÆÃÇl&÷åϯtò´%)JQ>ЖÂíŠ Šµ*½ç†gí3bDîpäÑ·¢ò<(Ó%B¦~ÀÕ ðq=|*:y ZÚ³Ì/À¿g¶- QjÑ“¸O}þù/Î|ȉzA+c8zôeõÖ¹·¤âßÃãDIz yê^倯­K­b¼h­S›?å}È™eñ™’ž´ZÛK§V‰÷ÄÄR¼¡ûwÇ@úò½¾Ï$1šÚö?ÿX†T3ü=Ü· ì¬(´jÉ{„‰¼rLÑß=.:¯qR&,• g¸ì€4SÄUe±ü&{Øm×Ïx„ñIÝ´×q:Ôx ÈàF¼¯¾ô}•‘s Íû¦±°¢)™Ñ 'S•9P™Á0¼Ž§Ÿ>F'‰Uï<]Š*Åf•òµýÞ‘Åöž(KàÈί6Zë.û¨\Uts*TNP”èl&&õJLÌF«†{2㺲V›?—F¥èxÓXò½—‘­@€{‚Ö’œ[PËp?‹ðd;œp9ŒjÛ#\;\O¯çSÏ&kBÌ›aò·W"*½ÂjS’¬ žŸ3¿9%=û:>³ZÕ‰çŒtšï[³NÜH­ž ¶y-Íôu£Y+¼Ä(è$á&ÿÅa¡#G0Ï…ˆ&t˜C{> ðòúk¯ªS¿:'m´®‘È%ÑÜ®Z"Ùuµ;’Êž2YÊJ‘½8zò¨ªø|˜¿:ó$’±Á M¹ûu2|Ù9QgNý:OÅŸ8ùA\’¬ô džÏ.±¿,ý “°¶~¿6؈^¶g‡v97r2DpkÎÌdSssýöÌìíÖôô—Ívç½v·ûøÀ6"÷Üw›òô¤(1Ìa,³dÀF£!F ‘¦R 9ÅÄA«Ì¹(#5GvÃ_¨>,´[ü>œfOM:OˆR¼o#Vn¥ó}SDzÂdrQ1=¥ÇwÎñGèL†¸ToM¨'\•¨uz *séu)êt5ø ßc43à¨Ñ¦÷¨~æWûia+ùЪ€€C{¦Ô!Kig‘é¢xeªKÏCq}11Ù!*¹’rÖŒÐ[V(Êûß´#’¶€iW4¤?™;_7åÍ][9âš[œ‚=—@ß]ƒ{•SŠ"òLé-è8ÕÒƒÁY*/u¸ õ!_ÇV¯Ûcç”Xg›g3D±W¥ûÎeªÉ5Y(Î^d&ËÄWà¯l%€QI »>{Ák­4:[ßè/3/sOL´,ªÃÕË5sÑ#h¢cõ:ÆÒÔõ”«¼—YiQ­ùO°j n„ûLSúa0NÊS)O°@ ‹XóÙ§¬K)ÊÂß;òê«Êåh¤ÈØù Ÿº<8w漌"“f%Ð@Ò¡.­IÅzC2`wÁƒë5ät°Ý vJW­1½EÏÕdúžyù¸Ê½µúœž`®ÿž´zÄmf§$M<óšê5-i×&e³/D`Ó¾US¶ýIVÓÚÚÎ^~÷í7åá^WÍù ÓFƒØD–%€AÎ[XE¶J-]oæ Ðs9÷ o©X˜¶×||¯ká[÷Ù™¨ÒK?‰xÍ|vãèI²\žÉSpö÷e›Áõ¯=ùm¥†þ¤â©ù3hÂéwÞåO=q^;©¶¯Mföxá^׌ƒÞŠÚÏA”%zQmjÖ¬þ¦MCSÇÇ{’HrÊΪ6Æ¥kôûÊ/‰>Ùv`ÊvÉÍ©u~efHÚŠ¡é·ÀNñ)±Îq!Oùµ”VÁï[Ç^QOŽÀ?õ~BŒŽšDÍ ¢›¨L_p¤msæ|ޏ¡…ÓënܼñˆÐÞËQ”>‹_¥ãè!5Š¶Ç‘š?Ø>¯µe¿pWe…ª—톫áÿû†îlÛuç×fæf¯µ§¦¿hNuÎ×ÍDtÕÕÏcquÌj4Ìíœ.UˆÖGDZ1ö9 êÁˆ¹HW;­`磓ᄭÍVúþDQ[UÚ4•±M™`³#§ß!çñüCÛ!l7A#è¡ãÝÀ‡Ò÷”pÑkÕ‹Mùp½ïP³3È`c8Ĉ3‚ÏÈÊÕ–kû¢ò÷ª3w‘4¯·UÚ“VuO–Ö¼ò•÷ዊ˜Œ¶à‚žI̽;6Â-õX,‡¢rO<€½]ük–™ñR–¸ÓÁ:ra‰†LR´%“²Î1Ù“v³‡ßÃû}“ZÇ5OJ=Ò[õÁ—ŸfžâˆÛe, Á¤ö˜˜k àLøy–C–ÖŒKè\5¸WšïErêzŒ†ŒÀõo³Cߪk‰E:¸¨ZÑuÑ —L§@=×ÄAñ,¨4†\V÷Ôæ·¶íƒ#ø¾~}…ŸHŒE†¬Ú^€±ŒÙa[¥—Å&¸TŽ´•ݦ÷¼JåûÈõ ÝR'ØÞ)*å°™­…Å: )ëÁë­På¢ðyŠq6˜õž4@`äwô(LA¥r³-º ªùnE±üâôYéÇ<éC`ܸBâT€$õL»'9ïW^~Må=Í´<ÈéG’m…ðb8ÿëÓòøë È÷ {ïQºº¿.k=O°[·¬”yZ%<¯P²Ê6$œ>û¶|‡•ûÍë%6ý®MdS_láDóŒN郳xœLqýè¡ï©M³§>~ö{Ò ì¼ –MzîÒËJ`Õ¦ÙMí¢:¯R»^aÊÊPÆfàWN~·¢V $E×”r–&{Ìõü N}ÕÕ Ÿhf2%ÔG¨düD©6-í{=¦óú×é7·0œ>ýi²¯¾ýÚËèÌy†§m"ö "»©î²” ¾¸pq)²cSMÿÊô#q”í‹Æé~_¸}cT#rçâŽgË|mÄgÄ·f:¶eÙ$ÓèŽ[,t§`÷¼ðÄpåÆ xóÃÐÀn°®z '‘Ðja™Õ™Ÿ7»3”ž¿Ñè´?šÍßõÆøö_åO)ÕK‡:EC2À¥B´þýò¨õz_£­3P6 îpŒsX²”Nw-`Â2Ý©ž{â¥Wü¼äÈÉÉêðûÀï4yÎc:ÁòÆ6¢"õ—SZü~­ÔÊivЂ‚þÆ„ÉFRÚÄ‹Þ*#X2ÀzÓ¿åŽÜfF SVJw2;2Yj˜ˆÂ“$F‡ž@DN½¬΃%J ž‰P†×77Ô;r㤌y&Å•+²?Þ|9þÇžƒÕ1 щSpªTi­'ó¤˜LF –˜h]±5¼Õ¨¼#”M' ÀUðcÖ£Uð,„°m®z&âÅÐDèÌ]†J<…êúêI)_eÀEãk©¼¦?™E2`³"–¹%ê@ìQ—j=â\öBŠ­î§šåɘ§JD©l]—9ÙäÌÐ(ɳå‹"»£Ë£a 0Îç9Õ°ÿà4¬¬šV:C0-£ˆ*¸0Ù¤×1樟õÍÓ:k„KYˆDÈ0æúð‘W*/@ÛÎøÝóo±±U¥ÑW!i[( ‰‡„¢~Çí%­ìÌ“U ó¼ÝËu=YeJòõˆF /þŽŠFdª3;:S²“b¤œÄ{÷2é™LEšJÇI´…‘oòþ‰6ØŒRÚgß’'Ñý¢ó°}oÊ&pYyÆÓ‰ò}*i¹6mr¢8Õ¬ÌïçÎ?3=äVJñØ3ßRÙ(,jê ~ö¯Î$êQmú¸Ò­x "¶®r\òñ”6òŽ,Ë]L”Hê6ðæ{g™¥ý­¾‡[ÀÕ¢Í5>ó¶u^©¬”¯¦/–Wîí’ÃY&žˆ#ýH4Îö¡ ^ŠÆj~e%j­`Psp/†ÑYš[KZ»~?†K{°gæf}®Í‹£Ýc‹éFšŸƒƒ»–`×Î]0ÕnƒðN)S*:‹ÆpöÝ_ÃÅ«WaˆN­i…bŸº!Z&šˆFõMs¶;ºÑŽjÝn/l·oך­¯ƒfëC¿V{'“òWÎÒÆc®Ÿ“a‘bëi¿¯_¤ÝÉ!Swõba¾—ížœ ±1A˜ï¾‰ÈsÃ;áÈɉÚ¿ fv. |㪠`¿Ö†#fɧ–Yþ (fKkX`£SªuŠhIõ?Ug›¦éfb‹™Ä\»ŽUÀD:m•‹õ€Êß+e—R)Ä)(êÉÂí®,XV#‡*¤Q}4Î\XÒe•ð zrR^Á)¡ÒÀþÓn#K1·&NI—Ÿ?“4§ö½0á$% ÓÌ7º ÊÏÓöŽd øJ;rÕOŠŽY€©R1šðÚpj„žŠÁ œ’õ& >bSkª‰+õˆËɨn™ƒö•MÃK“ºLªÞJÜ çð•uê6€Á×9úš‘ÙE¿úÓ²ŸšÒ !òxô§y­ë!ÐÀäe.ªk+›n§;4“Ii?XG~øÕ#Šœ~†ëœaôw†¥o ÜóêcßW9Y¦_ƒÓoŸã”ŒD{vüutä~’óa IÍ~Ú´[Ð*b£™k¥X5,-t›E,ŒàœoúÓ5•mH--jJ&ć˜2s=È\~KR œz¹é\Ÿ:÷ŽEqö´.]Fu:v!<Þ(ùYµ[z*äXBº6ÏÏ LDäÆ‘Çˆh%<ÿø^‘ F°‚_ý(†‘ÈËéÞªìÊæ‘ZEÔXŠVìná>|ŠNUÊmt”Ú¥÷4Ï@Ã"ºíú*ç2ªR4¾AÚÇgÎÉ1EiL17ÛʼnÊè’þ»p JN¯”ίUŠÍ­]£Ô n׬¤YeØË5yù;g在sZÄÈÒó ’倸 ôzÜʦø•uå©\UN¨‚pœ ã¤Ìžò,½¤å¬ªšƒ.’Ö¿-mk±4íF¡1IªÍ±û<"$Ë/0ŸbdÒª+väÃRªsž„Yñ`‡.Šÿ®/s½T–HÓÄÊ$o•q²%¢Ô€7¿&#·gÜd'¬±&R‘Ò›d}…PV2Trj¼ÝÐNvªJéJF l›$;p 8O„ûž¤â¾ùn/ëlßX‡£q:Ÿ$º‹Æ{ÿƒuX[5Y"*MP‡ÉK»0Ý©ÛEgÄ7]1i¢ù ùÄÛ¢R a˜ÄpÔ¤'s. tI­§:¶+`vÙã•å|ü›ÿé_ýËòYŽÄiÔEAž§Nƒ¡ª¦­CŒ¸ãñ>ùüsøô‹ @Jp&÷ùgaäºï›¹å¹3Çï¾,†°Èr¿îÜ»;æfàùC‡`[ËšµÉýèÒÛµÚZ£Ó¹SŸš¾>ÔúÞâ•Lˆ¯D|ºÇÒWl´ÝHQ|³ÿçÔ;’ZØ~Ÿ#Ïç‰SD@i~t¨Çî† "»¡؈R혌IN^*±î'Ú£ÊuqcH=»i±—¦•†ÜŠò ðÁ CˆpMi£ÕÑ8ýñëÇTÓÓ[h²™Ïô˜è‡ø×Ú¯°&}øÍ/Ïʪ3‰æd9ó[—Rü•t´ÓƒÁóå­g¡öV"r_¹g¤ õV]%@0YßÍûäµéXï÷™@F£ i9^?y\û´¤(ýðk'Ô©SgdfÕöŒ’›ebÛˆÒÂB¨Hæk,óH¬vRֲό¡r™õQ„ Ro9ï»ìÄ£¦„£¤'m4\*Õd…S‚ KE×aq¡ +k#æ¤é„ذ(D…)q€à‡@­µ¨‘šñœˆLÜ¢YÁ¼/@ŽKysvËfDN%GQ0ùM %ãsíº²Ì 2’9¹Pâ½"¨ùEÖÍóJ T2Š+ûÁE l@3ÓÒJ×’Þ¾UµÉ÷¨]SïÒýS÷KÊZ eH‘e’NÿÜ3Ѥsæù™& @rªÛàë5Ù›ÑÛÔc/JBLÅÈR;<‹¯™ÀG±eéQ B“Þðs&ëF9·piî‹|c¥Üº4Æ´J†u¥vŽNëCxöÙPKž§¢h]- ÖÞ #ØåQIÂîÓb–dURj‰»AzäÇæö¬‘ŸÖ@føÜ)3dgˆS·¡n/qù41gNÚ‰™Bnžß‘;tR„S~Q¾+‘¨˜²æwX,ÏÃç@À¶]ï°­j4=˜B°«0Ü'3å"O³_ütÄO¬#wD7i£ð±r¤O‘Ôœì6 ¯F³÷ïÝ…_œ> «««Nò Ø™—ÒíAÎhv³Ì¹¦JÍ“…:‹ d$ ‹­LálOMA§ÝbÝêcovºüú„ÕN‚Tûa_þšòƒe„·bß»yîâ•h-Qß4;‚Fý*=MŠ`¡J3“‚}r×lŒ"’#G\»M•©›™^i³+Tæ†uˉN9aÓ°^^c3÷hk©e#¤íp\ºùã#ϨPnÝ*;3\²XNÐD´×mLÕ zA§pöoÞË#è#fZ'™ÊEN’ymJ!¾¦Ê2®:Wš£Ï ƒŠþbò\šU9"ß;²$Ô£12âÚÐD7^šiU„Q®}¶‰ç-£ö}m[œgwkå”龊Ö>³.i’æ×ñÂz‘zvٰΕy'CLKr¤,Ç/Š\ÆG¥)6uaÐÛ°„´åÖˆ|¼rÁ(—(J§ÙÔ÷ŸwnX)²S¦UX} ›u¥Ä–“üx‹Ð^‘fÆxv©RÑp(ñ¨¼ffÀzÿxTcî²$†¨ •(š§OÚHÖhÈ &Ì”ÉRøœiûŒ3ajÞb"µU7i˜ÒR™‰IÜÝE<+äežiµë…jè©—™wž'Ä瞢 Iêæ<âZ5hÌ41rW ÑyõðyÙ6•ï/Y nì*ÿWf¢r7[£Ü…C¿6²z#¾ê±g†©”ÄWAT1µGÆIòÂh=†¶üá(Mwâ×:ëÎ(Žë½Qäé!~Í8Ï T|¦¶å®1Û}Ze?›68ÓƒÀte-Àý鳌´ð 9mëÜcIê³×* Æu´ã¡iGõLÄ®êIKS›&ñ¡Q ¡úÌ gÓ@Ç^ BÖkðsñëÄi{l%4¤fYõ«l®=|“;wnÃç_|Ýî4ÞÖÖVa„Me¦vJÿ–¡³QA` mJºØÀL€ &©¶›ì¦™IQ*ûëÕ¾i¸»"N:ëi†‚¤è”ç5˜G¯§ÂÐïLugÚ5&KÕ^A@Pk³¿‚¯nÞ…V»³K;T{f&nLu‡a«µî×ë÷¼zýšÖ.ãª~Ààwøý#2нČõjuh…5Ó,ï·v‘]^ŸÏS~6b·¬|m…7Ìä*£=¯=³yA™1$UÐa#€@:÷µüâƒÏäw^~ZÕt [LòÍ kQªH!Aˆ>üð]IS†:ÓSyÖ°¥µ1º`&ñƒÒé=Ø—*—×å´Ú”<Þšø7ÑžÇ÷¶èà!OÍ$DYÒ-ÑfP‡c¨ÃésïÉ ÄMï+Ë®ÍZg&:–Ú²÷µQ‚âl‡Öy ½š&Ðl6ù ‘COÓêäP(MéX"Ry[¯¼"™yC°’¯“ÊsüÄärN—§0¢;röLj+¢A°Y*#^djÏîý ³~ þÙžRƒÏâ÷Þ>%¿}ü¤"”þ·÷ŽŽUÂøº8zT|ÎL ÜöÈûÎiƒÉÚ(É8¡'rÈÚö›Ë|Š ’š_šS òÏ1úêªTv2ìhÊÎE1Êqœ—trAÍ<|VƳŒaôEÏTÉ<ƒf€‡Y¢>¿6R¡¤g¨lýu‚iMjŒ"9m§âr›W(88@%[kÀå uä<¼ÐhsµZMZ2´U²TÖ)ñ=‡ªTÿLŒd+ íÐ5i‘_›°©qŸJ’ÔṲ×/²8*Ï Ò:å³Ù9‹*¸žm¾›Œšæó[râ¢Ú#«<£dè¥F™ÍcÞù‚˜9CÕVVG 5 š[½86ÆO˜ça„î ð™c­  ÞiÅÌfï:Î3›úu‘•àr„eÈ;}„r»m™„À"8|:fÏFqü:ê½Ã8ÚÎra'ÝÁ8j¢(D±L,¤†vÈÏAIW„ß²q››v ïÝ+iwÐk#£1Ò b¼×ˆ§º9Í•ü¦ç1ò^¯A¿_·çCÀú ÉòÏ5oÌ÷1DðÞÄõ¨m“LbîÞ  XºNž·ëY®C ëÃ!ôG1”j'¡¤ eŠ“D褈œ5æi¢”m³¨Ó›IûOÌCŸg—|þé§pãÞýÒ sƒ"i#¯­¬2mÞ¤Ô}>Ì”r\ýœØíøß.ÍîR~%IùK:Yk´’¿¶v/쀖±0]*­ÐæÇ÷¡ZìÝ•5Mž™îÙ4¿'‹ÈØ)’QëY}fZuf¦ãùÙÎHÖë=¹…#wG‘gÈ`$³œ ²ýœcàœ~ÐDc_·T34Äñ8dGî²#GJà¥p¼&@H¬½J93’gP,yœ°gư10Hñ>Y„KOŠp•]ÉqÔ2ðH.•XGžÈ‰«ôyã±KyÉDnÁµ‘ƒqLI°#ð^Á ÒÚž?¨Œ;¬ÐñÒ$È1‚Y›MSõ|”ÄO¢½Ù—¨tOœ¥Ûã4™[Îâ©Õ4mŒq!‡£HÎ Ûø™ØŒñž ,Ów.ØV…^¯Ç6ˆÚ™køÅYcôCÂväh+V&Fuq3oŸuφý®Éí5<Àñ}ÄfȤk‡L™$ÒQ ©eZ²b!C+ü·†C»‰@×xanžÅ9¼†¬¬^ƒ^ÿ:^óÈ®³,•£L6‚À½Sƒß>ßÔRûïþû©GhèM|ƒŒd›ìHÌ`’ˆ”¿Rƒ¥­k/£óþäÂç<:‘‡^¸‰%~.m²næ!¢v<Œ04‚ev»©Ÿ“!ƒH5Z÷³\HÄ’†ò±â÷uøU5ŸÍøHa&ŠY°8W¿Ò9͵˜ú8”V» †0ÃSìPU’2ÅÐP{õF&ëØ kCŒÚû~.+Ï¿ƒÎòþü//âÇ}’(õµaéšÔ]àAy*¸4öTš(RË¢]¤•àõW^PaYÖqšäÈg=ÖÛðáo?’9ÂÅçz¸·¶÷©žV²‚ ž«¶Ù’‡säöz ¤'O-yŠ{÷,N7aùú(bã?Ù&¶1ªÌ%˜žžáßyK›ç±ŸT,éÏ÷Ô[¿–c¨YB¤ž¨®‚©¹Ù´uùOžZ÷d‘jÌ[ôʤÃ\h[‹O02GiQb!d.e%ÐûÎÌL[²œ†~ÿNe O–”ì\–+3Çá+s‘†­ùw»ý¬ ` kV,Æ+8®D¥,'Ž?‡Xo¾yN® §[]Ôä¢×Iã[¡›©NægÐîÔøº©w{u} ëýaÌòÅžsäBL«­Vå½D¤·+¹î»a¹—-E°õÐèØbYÆÆ9_¨I:‡îž£ÓŽ GJöƉ±Ï¥„<ŠW¥i­Sf3g]X‚-·sA10%Ÿaõ‰Hè~ÏSšhâgÌãïíMqmzý~Å’C ëÊoÔ“V£Õ[­a­Õî5ÚíÕz³}·ÖlÜðkõëA½~)¨Õ?G§ÿ™Å@ÚüC¿?{÷cù¯<£|pjbfÇ”öàCTxñã÷e 84û$aDš”4èõèÉŠ4ß±$çjä32ßFs#4tc>˜Ua”2ñ *kî‡qgj š­–ab㵞ùàYÞ‡#'Ž©—ôÍ·>À¡fžO©wØ]}’¤•2G3ð9ê3µ„!&2I§ôdŒ©ÔåvÀ’bZ€Ì×A‰;™*ÚF²:"dåºr>·½ΚÔLÔdA"vî&Ã%¹¾,l:Û\·ã pŽ5È’ë¨f€«ˆ¡ÍÅ~Þ©Ó¿‘JEÌ„oø ¿W”J“y)E:Ò:÷Í)è6¡;ÓÇŽyÐBÃEÏu0[rP‰Ëà“²ú&…„ÜCÊÓ.£ô$1ÓD+Ô?O)t7¨ÇÉ5S:ß'ÉÓ¤:¥SªD>ô ”לm1ÏÏWNÜÆdØët:mKˆÁÌ|hŽB$¡F dŠ¢£”{p#Ê1ÀÊrgå$ii(sH‚Uå‘“›–MÆÚøý¦ŠGø ©|CN’ºs®”‰åRÏhÊG±æ–_r¸Tûd¶§ N:œ†Ï_\º‹p޲%˜ÉްYº<bב&sQ&´‰QèÆ`£xMer•¤ð¾šÊ͸Æ×!yšn^Ûî;C_F¦¸$dDåº@<'Ñ¡(ö£½Ü…çgg¦ÄbœÊ™Dë©X%Íq¿¶>ìè¬e9KPDù ‹ &Àkû°mÇ<ŒG«èñYà9ŠèP»3ÞSFPtÆ. îÁˆ÷Ÿ˜N)rüíN¦º]XÄ5ë!ð¦k%_æ$e¥Õ´,¨£çEÙd/Ãç6ì2‰P00Ž< à^nàgGzloi¸uóræô{.H3N=1õ~üN*¤µÚ>Ó¬GÄé}oŠ÷ø5þ~óLÍœRí*oß¾Í%@ÖB ˆË`2ç´×¸ÖþÑÇŸâÃNa¡3ƒùèóÆp¯×çÞé±/yX~Qz<ЦFé4º­ÑÑÚ©™´ÔFÃF213Q ËÕÙBrêa`7¿KÛ8£ã"kÍ(&Û°RØD?±kq‚É:¤(&©Šó¶ÑM»²<|ÏÔÇØÐâ!«úÙn„i…}«JúÖ¥ g”Š2• ¼á@>:³³¶8í×ÞûôÓ©(I¶–AO©?gø#*l·µßhQ}‚`,ÂpèÕk=Y ×~zê›îœÿ¸Ûi_ÁO½ƒ×~7×õ/~ÝÊ: y¶õmë¶aÛt‡ÁEÍN©«"·Ä²¿üþ1^–ÿêí|œbRÞ¹"ãJd¿1n*’–åÔ æàϪقRÿ|þHhSÓøN|æ};]4 4t6rh`Ã6œ:{^ŽdÓ¦êt)W¦kQ‹ Íåœhý£½ÿƒo›{úé/ÎJ·ÇÜ?͎Ц½¤IÕçN¹ÌŒ¦§ÜM#V¶ïA‰DSŒ¥"§‘ZÖð$$Ϻ}á•WKlîÄ8GO*ø>ù®‚>FÕûŸÌð‡”ç¼ÇÍ|ÊÒ4>º.a[ þw<è³°F+TèÌ…eõB!l—–ô‘pYî@±Ð E½.C¡]­.O†‚œÍ\®÷ºÔí`¥rz MwªËÏÝ­$9ZdÌDOÌókÔ[ Í) ßûÃcꟕã>Ó¾Œô.‡´U ´CŒX·]Óöè’¨v^¯× šÚ½!à™ÍÂg Ï^ˆÈJßSçkê¢fÅj÷ŠI±[b›Íð5îÒÏ<öÛ“vΣFúÜV³Åƶ·Þ3@ŒI¹Öë”^7-›Ú²èéŒð„<¼ÎætÍnh®oØK`ÜŒ(«µ  ;8ÙÛ¢‹2œbRrƒ?Ç8sQÑ['å8Òc¡6ú!ØyûóxÞÅ}¸O µíÉÜ‘ÛHçÓf<Ýo4{õNØo¬ô—ƒ¤ï­ ×E“´_&4ëMvvžQr:kZcàt¯w¿¤Î(­ò¸ ´èŒ]»r vìØ ‹Û¶#ÈY†öã‹pùÒþ†ÓBeÓe»pó¤hG茯Ó´/í¤÷b£÷šÉövf§`BjÛ|‰7àêånt·°Q÷Üì,\úúk:eÒMb+µW£ CüLr¨¸²‘Wƒ¡®Áþ}{am}nߺý~¾Ø®“©c˜››Åç¡CÖøüCtÒÜïЭS¦#ެȈ‹2=3Í¿q㬮®@¯! %sÏèÞ —£›m;ÿyŒ’èéö† Æ™%jåÚ¸¥›¡0ŸêRëÊb8Ív"¡¤NxÞŠ*¼rp÷™Ÿùø3^$~‚ÎÉyNqðœX¦m¨Z̥۬“í`¢4õKäzæªÔӜٹ4žM[ÒáôGœþK‚’ †;4¹OWyß9ÿ½Ô¾§T³L ŠY‹õÝWàé§Ÿà(å‰G÷ÂÿùßÀ߉í4Œ;k*gãgˆt8ô“ÞªïÑî÷¼Ü^;©öDbw>Ê~th×<ìÙµƒïáÌ»¿Õ«±Ò£8ɆîN?ˆñ ý°>ر0·ÖjµÖR!—ÇÜÆmrWIï*±ož9°óËùšZY‰Mä$¥G—<'ý¯†÷Bâ#\·&ÍùÔtò›Ibfî|Þ,S‘u-0FgÀ¹xª!®£a#¤LQ µÝ)/äAG£r¢ÎZ€*BÒå¦]zÔPþs\5)ÿõSÿƹ24Ík\÷²5gm[”%üqÄ>Ñ–SÐÄ­(‰W•#*+–_@OÈXç’ÙR[†nÁ51¼C>¤ªÈŸýÑ157eºcéøtÝÿ‹UóãsrÍûÏFâÒ:rL/-! X”)èNCø˜qÏÔ|ÍDÂÌŠÙPöëîý®áÅ4t—è>BÖ.oÌâù»' †°®2´bfNò,·%éBÔÂk‡FO>+eiˆ@‹Ïœ21=4v^ <p Ø™·šþüŸ¿¦Ú³& öGõšê¯ øåß“œõICg‚¬%Ĺ‘ö‹Ç­\%r£«Çê²LvÊ ÕÑÀr£¦‘VË™Ñî×ÅF=øÖ÷N¨z‡ê®x-§ÎÐ#€“/ŸT¿üÉIå— A¿ ½¼Í_ƒºIªÅÏÍÍñ³á49ÍOL*{zÎcÖq©Êà ~~-@^P‡1Mò‚Fôøy‰!èér©CO´ÁÚÿ÷o÷ù¾ÌS!€ØƒaðâúpuvéÀNÔývúíË—.í¼|ñ«mÃæZ}܆ƒ•ÔßÄ'R[!ø¡gœYh"@*¤>ÚävÆ6\ ñûTeÝ(Eg1F™~„¯ßà¹Þžo€ÉÊÊJ^Ú3œÃ:'PoÔáÆµk°vö=´†«~øa.SQP·²¼Œ pà ÕÀ”QSç&¨Ø@;RG§‹`"Dg:µ}–ödp±±º +7oÀ ·fÆž{Þ!×(‰zôQøâÂçø}ð’Wº²]µzÊÑq ÏœP-èá}ÕðõSì§§§àÞÝ{p¯½¾ÄºÈF=ޮɹÓß)ƒCïEgxy9ƒ&Ïðs2!'üpßïÞµægñ~jpùÛ7q-Ð^!—`°P¨ß¡º8:Xjëêc¾FärH6Š6EíühËÐã<©Y!mG›’Áà¦mñØ57'_xQÖ/Ò#„ó¿~.ܼËèØËÕß°»wW¡; Úõ?<j=ˆ‰õ*LĤKénÏ*°I›Æ]ƇÇ|) x˜ÿõý×ðü3øÝ•;ÑR Âñ thÓùÔGHkO˜­øÐ)5yáÆ2>ôUXA£°6ˆ±¶ºF–‹ Gk]à ]Íe¼Æf-äT¥×£DñFø¯þâ[0;J³[ÞŒ>øìkuéó›þüìtÒhw¢Z£5òkµ„ÒÖ…ôÖè¬ì˜k]ܳkßeo+×ï.ãIïâ~¸åæ«ëRÛ÷•zÒ—ƒŽ(,€ò,ÄÚÚ×ÌÉ™S½™„%è‹'÷Zív71y I’³€ÙHáýÑ÷Ž«ÙFáÔØãïü³žTƒHÃßþ쌧…X‰ÞŠ´×d;Ž‹¶ø÷]Ê0+ Å@©¡·¤¸nœ¬²}ë.%Úi(ø¼înGó°lB m'üèO_S?ùû³²?’üÚ›ВҹJyE*ÎÖn»hH†ÃÀ‚l³æ s ¾ÿýãFÿJ˜±Ñ´®!~P³]Ç{kqª÷Ò…ŸKã€uNµàsŬí$5éh!6ÏMæ¨<1½àIªreFº´utà³33 ÞèïMû y÷Æ5Ý» ýq¯ž6èÉkltœn8·ZŒ˜>°cµÆŸÿå»À\˜ÔW—z=¢OAh.ýTúxZ=íÝpÔ¨7FZÈŠÇ}ŒDúÖ‚Z}µV«­ã÷ûÂïáeÝë­­ÞÚ¾céqFë ¯¡–FÈ bqâ*¹H~Íâ¡ùƒïœPM\šR&ÉûÕ,[­„<ðør\mÄþæ§ge’™!T“drËVÒºŽ¡Ü¯éËöÃò`*“7[¨*“ÁXÞÀ´¦h–sðŸýÙk*°ux2f¾Üš´I]~?úÓcê:°ÿ=• T^j Ðë8N®t@Z41¢!À<nÀýðˆšžÖ,0¡9½gf…¤G¢ð¼ãškM‘b€ÆwlÚ(›Ôu¨ÐhO‘©ËhèÜápŠ™¦Ù©41 ž»RìËW¸MÃÔT—Añ$è³~ðçÏ+ ^ß>}J¾ðÒIåÕÍ’2-e¡™€:£à‡~\ýÃß¾%•vƒ–”]môÈ•ÎûØ«²ÀP•Šår ê¢L¦Aoõkz™‰ü5â5£?¯½‚Ñ9¡tàÁ»gÞ“”ÆÅc+µšRZ¿‘¦É’/½¥8MR=^À=0×ÔEG;£½tZƒöÌÎÆ¸ÞjïŸýhª† Œk‘ŽÂÕ^ê£ÈGò©£B¤–½g> >ò\¿± ×ÆË¸vè¸02ߨÀí[·LçÆáÚÈk÷`Ù븀—>¹=÷GzwnßÏ¿ø{ìE¼¯NŸú;Àxè6øm|¦s ŒohŽª©§žÖ kR6Î7N„LB³îô¹-Îæ’}o‚êjX\\䨙ž?‡¡ü=ŒqÏ·8¼£bÎ9mʨ&{•¢#G®ð\^„móûÐoÌ#\å{ ²¬éŒ¢rŽOîÞw¼®!8È1Äc¨ Lá}ÕÚضÌ´€ÓüË7®ÂÍË—¸A©,¤«e.¦$™ô¼ïÀCpùâUèÝQ¹3&MÓ)p$)ŽàÖtÎ\Ðù avn–÷:­Ãýåî‰ì< Î@81+ÂÌtÿ}Xí£óN6xÏ{2à Às_åëݵ{÷¡ÏÎtÔD¬Cÿá¥÷Àçv14šËDüÀEbŒÍŒj'=˜÷“ƒÎSlNË×ý³šéúâtþàå`ÇΖy\ªÙà"ܽqÑ«aªù­Ž_]¼èv£ÁÓÕ(¹JÃ3†c˨’ƒö«BˆüýÖšI§I[¬¤¤!1×¼ô,tÑ©þíé÷é¹²¹z”63£Ù±R‹´ÌCÇLŽKò5ÍÎÎò÷hªS˜"ôG›“Årl è…C{áO¾ŒSC®nÌóáC;5Mæå‡Šö2±&3êlkðÄ-†poQ;±ŒU,V{CÑF7ZÙhœ²1³ŸU³‘×G‡7nÁ7×®0e¦(>gêã XûÞ‡Ëõ<öäÓZ ûúîK*ënÇ%N3|ö©¢áÝ ðVÒØóƒh~¶³þ䡽7—–––Wo}=ŒõA¯Ýêéö@E@§d$¤ß—‰ZÓý1žét}} ~xâÉþ•w6~sá^¦'£p›¼©±EZYfr3¿R§Õ‰x¶Ok·Ú’×8Š¢Ð/µ‡ÃƒÎøß¦§Úÿ¿¿¡¹ÆV£ ³Ù›ÊåËNÓ|$*ÏHÀgÝéLñÔ6A-ŒD¦á!ê°“~QX[U\{dfuPÔÇ-G+oa Ã6žÏ2³|,£)¹ÐYñÐ`Q6.­²¬@!¬ A››ãZáýûË Ú ZàO øýßï”<üúIEY¯”X#ÂÚ?üí;œñ=Óï[d*z†ÃÁíWv2Y™¶€û(RÏâkfðÙÎàCÁkëâß§ñg-m­³ÞU/¼€¯…×ßkvm öáàèfAn`øÙû°Ú[ažZ{ûÎmXÁ(ÿñ'â3Â]\“ùéøo_ýÀ§ÙØ£]Š„©ED·\ÉMë"%™÷î«"íiÛ,RnQ3Cèß^}âQxé™§XÜDˆÍº_„zÖ6†Ðh2D ¯‹œjô ̱÷=¾á[k=t8ˆôèXXœ‡¿ûÇ3põþ:ßÕÖêŒHNÖ:uÏMâgÑ*·±™È‹ÚÏæççóì‰#$†c#—ˆð™CûáþÍù5Xܾ£@Úˆ™% j›R4çMäÞËè6e,Ë-«ú÷ÍÝu:'Ò¥¥Ú2¥#ÝNN%áæ[˜GdŸÂ-M¤~¥òQøy!Õq7à‹O?Ó5)o÷ïû<;œ4îÞfô]Ǩûågà½ìf}zý øuÒ$@˜Ý˜Eñ¾1qŒêþ /ÝÀE_[Ù€Þ8†¥†—}ooOý»¿ë%±pgt£ÑT~*? •>šmj¦zCª,eBz© ý¬Ó Mã¶Tð¿Iã!Mñï°D/ÁçNsšHS)ñý0BDŸÄIyZÄݦ‡½ÑÝÛ×®¦4 d6¼· U´0, ËÎ+‘àûf„\¤ðâÑ8Ó«kCXœïHDöø¸XB—]áÏuÖ*(ú÷0ð8±CZ³f~~ú³aº³Ç޽¤j<+ÛSý`A|úÉ9Y«ÏˆÀïˆÏ?»„¾Í/œZ- ² ä9#¤ ðùKŒ,=¼¬šç ÚšøŒˆJø3? Â0SˆiUÐl†u•ÅUÖù©ôjø 5?CXêyáÝ;ýpûömµm‹íàæÍ›µ7ßøU0»¦ºŒÆ½ÖW_ýnt}ãîÌtm.éÔÛD’kþöÜÅ=Ñx„Ÿ­‚VÓófgk~·ãsÀ?ŒäÝ{=¯×‹ðš}‰W"Ãã4uÇ2o¥u%ÎԩЕtŽÝTK’m5åHNA3C|bàýÌô,l$CžŒ™1ÈaR«s[Ö£Û‡÷í…¥¥íüëÓ.l[ÜÁíuÔfwùÚשß}³¸ãõ`íø­Õ«0øÕ |ü‹ŸÃ®G1¥L*í­®‚!ç¹O&åM |™¼"`Ïž=&ØÙׯÈü&ÿ|ç®|-ß¿÷Ѷ¶T Ûµ¾q G­¾!>âpЂϑDŒ¸´Ö騀ª˜Bhæ7Mááý;`ÿÃoŸ.^þØØL;cÍüúÝ‹°÷@ Ü·o]…ß~ñð/_¾Ì5iîÉeÖžÏN—5Ói~·iè¶r”;ÞìÚô;'N©ijW;ùÚQȘY¼yšÂ|~ý–ÙøþMDy3Á휞‚}s3P#U2êeÇèrž" 4ÎÛf¦`ˆï“{+p7$ؾv"ÑÄ.[`§TAIø#šò†~#”@hÉ2VT[GçýÕ—_ÂþƒÀÇŸ|„›y T0øè«+ðÊSqÚ2d¢­¹4ƒ‡²4:ש]ñ´$²Î¸†šÝñp€€ ½Õx+XB'÷_üåÂøé¯àókwq #ÞØ5\cJ³b TÈHm˜M¢õjʼ]Cëò$#“ΤH@ÛY“¿½¼ÎÂȤ}z‹ÿmõiw)†Ôp[*4%| hø<Ðd¥ßƒ{ý!ôc–¥þçÔ¥¿KB-·åS E!®><‡`ƒ~çÖí;6¥…÷h€œ&üô‹/aߌ„9O˽ NmÍ«EÐÝ>|÷õ—avvÆö (CN4Œ.ü¢¸c„‘F ¡âˆs!ˆhàšOµ ŽHšTƒ‘ï­.ó³ÉÓülLlÎA^çKà6WNóžñŒP—ä,‘ôf[dE9Î$†<ŽÆæÑPQ„ˆ}tŸ÷îÞ…ðpF½>¿‡Ž [›RÈtÍ™rÑ¥«7+¸|é\½v£°³a9h3R®¦Ñ¨á߉MëówJ÷’±’¬T§a ôÅN¡÷aûê›Í ‰ßü¶)Ð…õF!|ùÑ;ðõ¥ÛŒü‰ÔÆè‘Õï‚z.A›Yͧ4§òV1CÞ3m{[1I2®DHbpŠ×D] ĸ¥³wí’€§Ÿyvm›?üîÞ¼ž£õ XA¯xÏǨ&8Éz ~þïßD>D£¨ÐÈ×йLþ‡à¹g(B­¡1Eãö^ Ÿ_X泑Pû”¨QÉŒ?f€œM Q”â´ Aâ%¼ª5+éjfQÇýµruÍ¥ô1¨èÝ0ýÈ÷——¡ÑÁ¨n]òð]k­h*gH6qÈ{¿‰Ñ™ÀMîQªx9æòéb/‚íèh¯Òxh{}hqaWg–×΀D‡a&³™H˜ ¾ÆéP†‹ÊP^ݨ’ƒéb$ùøsÏÂGç‡ÎÿÞß”Tôèûpñ—ðøc1SžÇ £ ¤Èœ+cªc˜ö¸f§Á,ï.î•q2‚ípР±£ÝíSx¾ñ|ðÌôðLû>3€ŸÀ„(íÛ ¡¬¸6">d{ˆxïþ}§9 />Ï?õ"\úæ¼óÉYظ³ú>‚”ú§Ù P|ýÕ%˜Á(wiû¬!P¿wïÜY½eí €ÖÔl øŸÂ}ÖÓ÷!¥ô¾®³¬÷m}BY|.ï~ï/‚·¿1¼ŽÌ¨,Òù¡£¼²bò”»Ðq§Ã>ìÛ»mÃýÅ*_.­ýÂ|n-×òV6’ôV©+¢Ñ$F“_¨C Á-‚¡!;zQmMþÅI8Çx?½ì™^‚㯽‚Ÿ½ú9~ÂfƒAkðÕ´£ƒ58øÐ^ʼí÷o\zÜô?'FKÙ¢;ê'¶ ¡#j] èˆ˜wÌ L“¼6MÈ?}lç"§&xT^Éñ ’¹tóFìu&ÙMáM.u;pZcû"À¶mh´j‚{–`áùç`7þüÙ}ÃÑ—_„§{¦ÛM¸Ûp´\n™qí>™•NU¶¯›±Ê44¤ Ñî6Œš¡4(„jß!«¬¦ðÙůùsD }æcúˆ …{dû ,âáÒv0¼ëyåT7õµã¡Ü÷È£LF¹N5wK2TªÜìiYý–(á³@FÑŽ$ÊJ%qaIa¾-;VX#J‰dSã:%9ª±ÊJÂHoÒÚîÛ»ºDνó[øòÊ=ülÓrf {1 Óý¡¡ÔOOÌTúN„¦õ^âqdÛ6ünD«¶ÏµìqÃîØ±üv—÷‘…ˆSq`ï’­ÀF÷S#ÂM3*~aPÌÌz& ³Ú‘¥¨©AUĽŴyê<¬¬ ø°VÕ‰—å¸*×n܆þ(a°: ýŽo¦ð9 ±¢OK tçï¦%“¢:Kä1:ç¨ ÚÐí6sª$éØŒ¼g1>z©² /Z«Ï>ù®_¿}Üu•zäZÛYšOš3sT>Ó­Ë}2A*¼‡ ¿áÚ7­\%žÅ I‰§ÎýÓT®!v2+/úžeöÚA)i–ßt²Äqb¾3[]“Þ@bÄ•èy&Š•ã¨»Á q­F†¹ÜDg°{÷nn«é­ã¹„Uˆ‰ MC"ä"üòoÞ‚ÞÚRuîtj°÷¡8rxÌîØ 2Üín æáÆ4n‘™Èd!§Uo„ÖЈů„§¬¥d¸ãŒŸaJÑ0ji¬š–e`ÚDkŒÔã>‚“»ëìth¦1Ç[SšÁ× ±í²™ÑÚþáó'è4Ü¿²bæ„™é`pâ¬,4£MŸ9ÒÐFðJŽ€àéö´±§ø¾«è¬è}XR4°íFö¹˜ÈÚÊš‘ÅÆßòÅçà•WŽ!Ø™ƒ=±]þê*D¨èzSʬ #ÖE­Áûöíãk¡€žÍL á;$ZS3zé: ¦fçÁo˜ÚÑ„]/퀽G÷ÀžC» 3߯ yÆL|÷ÞÆ‡düÝH§¶üª,ŸŠÀ‚)9 iÌ0>ƒ?úÁû÷d^Ï ™…mÛáúø:Ä×"Ö(àRŽ®ÎJ AÖB39²4˜Š…˜hd2eœÑ \„ßD¬p‰”lïüì¼úÂQ8üüQ˜™š…o®]â½áνý÷íÙÞ=û y|/Äš§žôúCò<…M‡¦”‘y6^ç½Íö|þÙpó&‚‰þšÝGº$F" >.´©uIçÀêhåÃq\†Åì-ŒUF×à¯Ur&ô½‰ÄèPKÛëkž’;¸ˆÀC€žôâɉP23á^rßìC¶ÁÓ/„›×î2Ù§àÝu¼{h ZñíØ¹ –ñÓ>Éâ5PÍüögW`ånו™úø¬› ¶moÁþƒ l(ÑPPB“Cˆ»wÖÐÉR†Ð¥I½œ KûðÖ­[Fú3N¬‚WÂdSÒ€Oñ9fãÈüÌ€m+¶á™ƒF¯%6¶› O‰¬z3C`èñO-NÁŸýw í™,¡ÍjÏ£¹y¿3ÚÂöã9þ 9geÇR”>ƒÑ:9u>“T’#M´§Ô’E÷O­lž,êC¿‡¶jƒ#Ç^‡={öæ$È Ó=Œ°Ç¸§hªbDÒ©ÌÞ7”Zô‘GØÉóÙÀ¢‡vyñ¡‡ðýv@wf^~â/àÉgÀÁGŸ†Ëß|xè•]ðèôAØ#vA£U‡±‡Àö~ŒÎ6$JÅG0ÑZ2Û›À¾ÄNŠË˜ôFçÚ¶^|þ‡°¸}Î ÛR‰Žþv|6V7zÒèYPÀhe¹i ‰œ=;;Çéês¬TZsÒœ »ã×M Ôu¶+Ô~HàsÏÒÃpýö-8uþç\zqÝ|æ”ÎÛ6Iaçöýüþ‰ßÃ`Ôý¿‰$Ë›°€×1»°ПõCôãO~=ùˆÛð½LÞÂu¿Ã<š)À´¥i&IÛ¶Éþh º Ìm¡Ï;´ï JÜY»Á*mg_³‚þdü± Ù$ƒƒf 7L›kð&gé@c7U«ÁÎm ð:­O®\?7õ"o¤wQýìâõÛðÂCVšœ7@Qào¯\LjÖhSëõ_¸aîtø<3“q<5e(¼žg$L¥1ø?yã4Â4Äõ@ÙÖ\Á7Ì?ÿÞ è Fðÿü,d29 …0Ãï%$ªÿçpäågÑX,ÁÝ»w˜‘ôÙí|HS°Ø2B(öý)"Ü>×…[¸ùó8×¾`{–©˜øöÇ—àÝ/®òF"£¯,B¥4­rÃrÆyñþæ÷È(5ó‰qùØ?«ßÌ%Q;¬%/aȲÁ/ÏHWP<²~͘?m•­Üï3KÞ£Ñ ü¿ÿ:ŠD‰ò¬Í¤ªrͿԹ£sêo¡íú÷3+rÂ#ɱ´I±ó¾Bç:ÂçÊÅ pð±G¹[Íú ƒsï| ß:þŒiïb @Æ*ºÞ> 1¥ìlŸy¦IÇ ðÆ:·0ž?ÿk¸ðõMÎ"Ѱ;B®“h³â!2^}å¢hê }ŒŸÕxh¿wžº‰hüê ¾ÞÕÞÑ:î‰!±[ðá'7 !G`Õ™hÒ>ïtºìȨøË‹·`j~CüÀuuË+}Dö%f\ã¯Sy ‰JD!T%¿‹a'¤Û<.íi ©L (ôÑÿ¾Þ¬I’ì:;¾Exì‘‘‘{Ö^ÕÝÕ+ˆ…M @Éá6”8¢ %L2éAfó*™éA/2“õ¿‘I&ÓˈfCrÈà €F7ÐÝÕµWeå‘{x„‡o:ß9×=¢–VÙ™‘î×ï=ëw¾/3dMxu$>){–­Ýyk6¼| ÍMô@ #"HÓb>VÉð\a‚«µ5ˆ 8ˆúÎ?ûPJÉ(Õþ»óc5|†¹­RÑ#÷î½Áâ-zþlÆÙï‚^}oA—½‘”Ä¥•’)M)ÖèêrÁÁ„ß— ˜?Ô}œ–¤©I[ö:¼€.Kf "5_&¨„·KÁhNvìʼ¾R亨8NÀj­-ÑÉËy¶G«wHƇöÞ¼A¿ý_ý®°öˆcæµ}ûëï°1oÓþßOÁd&%¶ñÚø&ðÂ~Ì©?ÉRÉŒcÀ¼ü³]þÜ•­ÏQ×cMÁ Ôø{ßú½yÿ• ,ÿÿüj$U•Ã7ޣѫ't28¥ŒÏ°•­¹Û?þøgôÁ(1A¹Fï¾ý[ôóÅ÷å5²ø3^<5ãW:ï^"O„<‘5z•2Ÿ[»X7 ì¢UA‰œ·Ÿ •¯£Z†¶'I¶#=äÁømsƒ=Kf®üôì]žð5WUO^’Ø düH$Ïmzòä±à„ö9ƒõJl¥/`ò”àá$‘ˆíä@ƒâœèjA/N_Ò_ÿý_p p°¯k’“Q-– Ã"àúrö9Ýë±ãwnߦGIÉÛ¶CÁ‘Ïä=ýÝþ)ýàþž³þ ŸûcÞ3¼×k¨CíÐÙÉ>}9ÿ)Û‚i1ö&ÕY“LˆÔ¶[ã}± Ç§èïJp„gþÝÿ '‡ôW?üS©ŒÏ§[üÙ=¾Æ)9ÿÛÿáG¿ó«ß¤w8cüà»ô+oÜ£÷îÝ¥ß{—Þåïñ_;:¤­NGNß§Æ‘ÿ£òšæ+äÍù&gäÍfýµ¬Ü–Œ+¡Ÿ|þPË%)/;R^¯ówʺ)$UÀIÇÐÌ‘ œ9ÏÖN¼ÄÿõgE/ú—RòHÒì?Í»nÀ’@ýÿâw9Ón±]Ót<¢Þ<,´­­µ3o”XJÃwïÞUbðîb<¿Çè™gÆ|,C+xÑÐã³+ò,¥‚õlGX²|)U“Ü£ ¿#ÈgóÑ*ÑNþ 3j¸É2¤z]ËiŽ"r-ƒ2׌\ÛJÀ³fÍˈ‰áW—¬-I r›µî±ÉÞSe¶[³éE5ùýüÁ3º¸älI66€RôKAÇæØŽo¤Ÿq0ª>ÐÓU1ÚP‡::èÒ£mºyÜ•9гþPCøZªBl‰„) FÀÏüÍ7nI+cÌ¥ôüôE÷cWJŒ,„‘F0‰ÆÚ­ðõ³ø7WÑAýþ~Lßû‡/x–b¸QîF Ù5Ù¸ŒëZ)óþýàƒ»lÀŒšöP˜Qýïg>Tn”˜ñ”*íímÑÎN—nß9bÇÓçuš™qD»è—#Á瀠lfÀA¼zþ”þöã i< T”‚ØøQd2Ð Îq[Ún!ªN¼ÝªÑ÷ßÿ 6bUzúüR"{Çq ´´èË;nAr‚ÌPå)QîNw‘gñRŽÏÆE%'¯æ¼N´Ôë]ò˜k­ñúÞXÆyò½gѻ߸Îg½D»{´½Û ‡ŸŸhûÁÌéK Í×3ù|v8 <”ñªN3“Ñ$ …3Ch“ ƒà{ÜÜyë˜ß ôÑ0qÆöÿÊ.}va¾Ë«L¦B„J‚¾QÎ[ïR…ŸáÎþÍF…‹*“¦-álpT¨Ì/¾o‚“(8¼U)ÝþðMúð÷ÿ3A£‹CÄt@ÙP¸òúß÷-šŒ†l{´„œ˜9ü\DJõ&T nþÌ”[JMeÖ’ÈyµÁövíÄ–ßÖ‘:<ãˆÓÙ’Ž÷Ž©å×èåÙ3±I,6*¡‹ó½ùæ[Ú"…Y§¾IÁrJ ïª^“¾xò1P§2nr˜òµ2…ìÀ†ìà†/&4?›S4¥Š"¬)¨bŒj$JìØû’ý“-óâ„6ËŒ»²Ö©UÛ6”¯DßûÁŸÒ”æŠI™(êÝ̃ëoÍ´ î €f/ ÀÚ*t’:8{!#+•]å~OµmôðÙ˜^™ªÖš^Y¤¥3Ó HÌœ­\ö 3>ó»R i6‚‰Ws¶ù°m·¥ÄßàßM&caVl4Žxßw„ĆƒË3V!’`Dt.gqiZQÒ2±U[>Jçä„eX[² 5ÛÙjÓý»÷éêŠ3qÈÂÆà‡äüëßý­àlšldk¾om¶n&l*!_PÀ;ßÿøS¹Iû?¡@÷†·«eºqýº¤›Î‚+ŸcdË]—­°‰dÇ«ÞÂâ#ªÄ]óë¦í†xPΦôwõ}úüì‚øÔЩþÿgâÔÿö÷¾K¼ÓW§Ô»¼¢zßL—ªN&Q¹ à•ë8à( ?»uó&M‡õ£OÊ›(YÌi§SdÄRªa/ó÷_<£2t0¢|u!ØñèÍ:GÃ5Wæ¢EŠÂ¦B*ñÕðÎ^#Y”²Fˆ驸ƒ™=´l#Ê¡erB]hhS–O7ðêÐSc$RC¯ó0¯Í¹ÓMeetÞÑd6•R8ü“ç¾î¸óóæ.½·ïÑû×tç¨Jï¼{Ÿ>üÕoЯ¼÷&}ýý{ôá7Þ£Þ}ƒƒ¤Ûtpý†Ì´ÁA°U΄|7G¸*³ú˜ãá„Ì™oƒ¹8ŒáU£ïm9ˆ‰Iï\Œ´ðó$6\4ã¬|ÌA_Ñô ýÙ÷>'sàÀN(€9Ç}M(&– +¡o|í–î}ìÁ W¼”^ýðÇôñ§_ÊÄ,Ìtb;àÜ4ê6ýø§/¥¬-G+¿7fA½ÊûƲZÊhÊÆ¼ÕpP¢õœÍ”fdP+àªZñDµ ¢'8'o½yHÿÅŸüsá7èt·y¯véô´Ç{¥¦™®QúËgªµ¿k{#Ϭä:Sq¤9˜*1$Ç–dÙFàGjÛÝ ½÷õ&}ë×ß¡ûܧO~ô‚¯U§Ћšý¬òîÛÔh¶$HØ?æ„€ÿ¯#š–9&ƒÞ`8ä d‡3˜] q·[ÊЯ—ë¬ÕøBª¾óGߤn޼Ì÷ScGפ¿ü·Ÿ˜ B3C'¶™%£?èm­Tâ0ÝÁê ?ÖÒ&>¢¬N01"9›nœž+'(Mº@ËòèÞ7ލsÄVË?õtË$*pØNvæ±3íÓÛÍ÷9kžMÙé] €qÖ²ðf‘(°Cù>”ñ߸ †ÉP¹ OËÊŒ6yVðàw[¼n‚e°L‘Úôèy_ð>ßÛÇÿð7Y2Ð`€‰Ô‹-aƒ„2ûµãk°Œ{TI·é¸ó6]Ø_Š=~üÉOÄæ&pÆKÎp/μCºz1 ÉË-ìPVÊ~q~ÎNj¹7ÛÈ®d?‰–7~èkÇTWìØ< ªåýÝü+êÈ=ÇUm Ù#5>–QgËLF¥#~¥‚ç>'*Ó¾|Nœëi$™¦ž´w`NÏΊÉu92¦wï|À.è•ú™œø¬YÒßÛÛSiV>k(»—]ÞG¼ †}zþü!Û­ž$ ù³ÓnÓ|–ÑÕÅ9ÿü ]?Ú£¯q¢|°w‹÷é¬wÊñ\0]§P½ƒ]XEcÚiî«:›T`Hæøkþ6  ‘\ó_ø¬”µÁvºù¥(½ œœÐ€3cû+ÎÛ3ÿÆ&óFàƒ·ß0‚%Æ1ñFûÅãgtÎN¤÷y/1W§Â‚£tk³ÁN1vÆ‘b|ëÑN|N~ôSúó/Ó ÑNœ¬×,ë5¯²I1úÎõCjð>ïõ$+ÎYávz1œÉømŽ¥¹ |Zu›5ÚßÝ‘è3XFTKCÚâ©Èæù>þöãTYpZUŸ9ý }£[£[-ŸöëG¶6Ͱer˜¾ÚÓÿ%ÉE¬7o€E ÉlP'—ÊDÆ+êsJ„p$Ù–¢Äc3I÷âU™HùM½ouú±öœZRؼ×Ìå뺌ùȬºÑW7%ê§øëk7[ôû{Ô©•E]çu¨9œµ¤ùµ¬’8ÃØèo§B’Ñ‹g'äðõ»Vjds–öv#)­é7îˆq\bÔ&f£ßïóAh CH-ð-°3O+66+úòÁcú?þÍxoªBkÿ¥õ\ÂÒ2½ÔÜÙa|ݽ¹-¬Ãx<¥'O^ÐÙƒ‡#,Eî¶ÏaŸƒRôèÑSO9ÊÛÃNʶ4H*fy‹À B 9 öªO~}›ÍÓR*޳!ÌaÊè΀óÞjú´Óá½ØmÐ.g__ÿwéwþà7©*áTûçõ&¢ö€úWK1NøÙò§…¬ê\¦p~ zzÖ$ÓëMòìÉ8ñt,šNy¬—°3Þ‡ZÝ„×Ý¢Ÿ\h`ªsŒºžãõ;´³·cÖ6¢ímvèmúâÓ—Ê ™ÇcÀ•q4¤ãã=ÚÝÝãs· îV3ì@øÐÎÁú|÷Þ£›o¾Á!æø0}áÐ Îx?ýÁ—ªßnŒv.-‹J g.£é@„Gr¤ý¦¯‚ qþM¢°š /Í ô>*xµj“¾þ›ß¢R-¤Û#§}D®ÏŸí>{Þ[/¯Îè Ó¡ë=©b=üìcÎfµíáájI=W¸Õ5´ÜI?9‰‹g’„‰x0ZP8ääárE««ˆ¼çbàERegN?·ü@Øk†«üãÈθٖ+ 1+—y,>ýør¿1?óÌÕ~3Ó€§Óæ`ïÚrjejvšÔh7uÏoT+b´*Ð.Ñ@5ÉŒ\°Ñ‚_iÕ Žmk£ù[þsúî׿C%ÖžŸ>¤4t8PQ%4¬;ªEàñÀY@Br%´±ç|È8ËðÍ®R ^†€ç‚ƃ¶¡ÏøÌ]ÑŠ³ñãƒCƬz½Ö¦ç/šê²âlªÁÚ¥ìüG´¿wÀö—m'ÝÙ] 9ÑM€/kÒ,8#ç_ýÚ7?Ú,}ÿcN¼ø2Ü럽8Q.cS4ãß\‚¶ò—ùa½uíˆP£à0¶JeúË},FÆ52yùKJà¼aç¼Yó]p„y c[ü@ýäúóO¾®æeiR’,'1 ©œ¿Í–Ù딎ø!nµ(å…«ƒŠ–i“ÏK‹¿Ú| :Õ=Ìåsó1#ˆ ʶÕ8ÿÇŸ)(êgGlUé÷tï C 6Z þüvì;(óQ*ñ£Xñ&Z&J?šfÖ?ÚßÇš$…»´tŠR!ÎE,Ɔ7{Ð%GÔ9ÎÎ:/ã뿉¢ªsã`"VýJ ñ¬%„q¬©l)© ÖH¤‘%šŒ Ïú°šÐ¶CßxcOÑöëqP êŒ\ŒÏ8Úærè•EÅ -•ï÷Qïœúƒi1FWˆìXùž´”t9ÓýûoJ¹ €0Œ¿œœÓƒÇ§4g£ÎFÔ»âÌŠ“‹þþŸ?ý;ú'w].V4c'@°¢Tà8:SÒÌÇǤ‡Ï볿ãR«]§Áå€zlÜ^œ²‘ëQƲ‰’(DF ðÇ©í—P˜‰~ø“çŠÌ²l­ftðÌÀ]«pðÁZ“r«Ò”ý*²¤,éØÚ·.Cr´îÓñ^ƒîßÙ¡·ß>¤ÿÙ}Ú}ï-Žò<%L‰×YôçŸpä¯BD‚6·Ý°ÁÞÃy_pV DbƒjGb˜Ö{EUÁd?ÄÁm?ÇÕ}E® Û[œ½M®øn‡ÆQãyÛ¢€¦eâŒö9³»y$ï)‚=ü>p·ºMúìãFLW¥¢•‰^ÿBzÑ`;9éQ«Þe™ ÉÈÁí}ç÷“÷¥–##Þ !ß{ÿ|DOñ\2<×Y·Lrö>ë {†\Ã*0Qù¨_®¸–bNÚÏ%¤¹S ^£ãl|œ2Ÿ<¶þ‚Ÿû’Â;é²\£$ ü²‹å%}cïMja†;zIþvª• sÅ‘å¥ò—€ç€l_PÍq²>ÇèXIzv󊚨ìá™ðZÔÛ[0‹Â—̹÷éüÓ) Øáò¹AàSX›TûÁ©äæÙ2Ä]÷„¼bõiºS6qèäì¢ÀY,ú<ÂÀfZŸ*ù Þ m¾éd"ç<õy¯ûuÙ§7oܦÞÿ=}ø¹¶õ0¶g©”®mœ9* g'èj<ÒêAMG†åÝ'D÷n}þÕù¯éÞ;_£R·C•Ý2mP¸—8¨)¸SyB“Û:UNäµ (gI »É>ªÇ‰rÒÄôáû¿NßþÚ·…í`{‡|·FÏNžp0ejš|PF‡®Äb¶ôJ«®Ì\_ý+¶W&8¡H—‚évvµu›|$”ùœl¾P<[¸&n–àxgìwöw$ÂÃ{ú˜‰ÞˆmR…î\¿Oο|ÿíÀ+k›¾ô¦#§¯få±÷Ù—å@øÕ勹Λ¤ ¢oÞ [ü *–F}P‡ºÁ‹Œ¹¹ŒïðÙ3úòj㲤%k³çÇÙ8áùµÈzúìD}ÎîvØ‘>úéÏè/?y@ýá€6Bpâ˜Iÿ® lù{ Ù!8ìaΕ¯©ÓjÐ]þì8n¾Ð6ÀÍŽŒŠÊuã­ GÄì°s„)’¬X8¾¯A9Æ"£Â~ô·¨EªÔŸÌèZ§!½¿ŸþüÍÙȱz·éÑûl¤®íï}¼OtÄ_;[ˆ68ÈáM³³ÏŠÊÙŠªv,A‘ƒÒx–«Ã­ýPÐû•|e?³ìµ¶¶ 2͘”Í[‡3H”øÇóPJBXŸåŠ®Œ€¨sGI{T+·Á÷ú¯:ùÈdï®ÑüUNz» ÏHqq6à˜)ïú Õ8{õ2ºû@€%Ùš ³ÀÖ‰¡)"SZ O8ºr`ÅN‘ ÏùYŸÉP(Aó=&ÙŸá¥"Cøƒëì_¥Ìîˆ4äL/˜¦;¯ÞQj@_>ЗOzôäyŸZ±×ùk:’t[%6he™›×ŒÕ)ˆ rð“Œ,‚G ˜ÐË—§ì̇ÔôW´ÃÑ_mÇeTvR¨À\9¨Oa´ÎNûôw?z¬ä 9Ë]²A]dÖÎÁá`¦ê,Õaz[ÂÕЩõ‰}ºI9M$(»[>ÔýÚ{ÇtôÛT>@f¸Ð‰†rŒQAÿÙ-Ñ÷þúcÉOaœ¸Vp”¿åÎ|`¤‚_†‘86 {eŸà9Eê<"³O\ἂvR)PQ¡ÁÈ×B2›áå” )(mÞ‹û‰hTM€×»ùÆ^ß“ò= ö’Æã©ÜˆôòéHÈ’!˜úל“-Ý.gçÇBçZ.5ik{‹jå=ÿôœÎŸ\r@7 «‹>Pvè|®ÎõÕ9ZYwdc•SQ*¸ÐÈ‘ ý3wÛD¥]¶_‰d€«I¦¤J´îIãu2a¹ãIJ±3 rÁ#lIK¤]vì6aÏs§²Kò‚öv,¶ m²wØLœ1bÖ܈@9E»LéUo(Ï#Ž@Ô£Ò&hûPh˜è‹Òš™dA5L´ÛÙ§¶Ÿ?xHO&ËT ­ðI)¹2gô08Â]™j $‹x_Ì—Sºñöuºmš°Í]„cÚª±ÓÄ8^“Ïy¥,2še´8 Çëd¿eºÏA-ˆ¸ÞGts÷]¿þ6Ý9¼-×}qúB‚W)q›€KXáØîU«uyè°a“ùY˜ÈVÒ¡ï|ý÷éäê‚.“9í½s‹Ž¿~“®ßCÄ]B¬#Œ¸Éÿ¾Âg˜FÐóHdò¤&Ä8yVClWb˜Þ~õÝïÒo|ë7$yÀù¹œŒéîÁgæ5:=½0ø&•ÃÃø"lÕîÎŽœ¡V»E~½Ã{™*°x¹¡œK×*K¢'c‘¤ZõÔlû”‹ îv·Ø¾ h<Ó êÈáÏ8ýãg›FÚnE‚ñâÙ9-æ:Œgêü–G-Wšm{Ú-O'¬1«…:ôOO)›Nè:¢¸Á¿B] .ë*t~‡} øN>¤”3¯ÞpLRÕv=3ÚRdCÚÿØœBŸè6_ù£‡é?|ñˆzl€ ƒŽ€½z8pDpQPÌÉAéDÆ_2^È ošcÚâC[·©àU7ø­µ¬)ßæ,zÎíbj™ ¼ïp* ”ž•J’^¿q] BƒÊ0âëë‘ú—ߣC;¤.œgéû­&5v;dr†ºß%BEÁ oL— éåd!U¼ÌvBm7¡º“íkÎHØHpæ½[»Ö’žUa”6Äêe|‹ïyºˆé¿ùý¯Ó¯þÊÛôÁ[·¨Ë™d‰ïðp»ÎÎA5½j†_ÆfÎs˜+›Š ­n¸R ½²Ñ÷Àˆ¥Ldçk¨räXõÙH¦ êx‘€Úv·[t°¿£jWYÜ“‘¥_g=& À}Õ,vˆÁ+¹'¯¯ñøÇ‰Ñ}·×â$–¨%4NÝ*ˆR©H¤†³¡-‰$;ß¾q|„eŠÙÀz6¢å¶ÐdÂçj{yií ]A)€LªÜWæëm×UÚ´ä+¿xl4Ä‘9£0SåŸ'ñ‚Úˆ*%ŽÒk©h/V–é3ç¥.m‹`Îб ÇèK†ÀÉfÔ`»·ÓásÕk•Ãin·éÞûä^ßâ=ßäëCÙ„Ÿ_KÚ 1ÎËå˜üCÓ‹· ÖA©Ye\'ÔÌ£cy–®À¹²¥˜É0 ,9kmqæ÷|ÌçD™ÿ`·›[ì ë™h§ï_ۧ݃®Ì ‡‹ÍgŸÓ2¥ÄoY3ªÖxÍç+a=#£Rg$%$!1',G¨šë°(±cÒp9æ€u’Ðè$ ÉÉ‚j P)—Œ<¦S°= %犻}Ž$ ËMN9#·ž (MÇL8ñ¸ÅìNª=~(ÇÁá.sº#ð™Ò5Î%œ æý!xJSÌ×tzE“UŸV¨êµm:vÚÔltÏÞ¥-jP³‰ Ûµé€ó"5@>`EÕÄMÛ>yaÍbùìp±’ç"ýs~.a¦NÏ-¢ÜFà=à+í†ôàò'4CKÉc'^ç¬ãSkw‹ƒŽª”k#~ŸoÿÞÑÅÅK¹¿H|ãñ;T3…cˆ•€ýPAó›ì¨\Únuio«+À2ôk%pµ•"B6|O§"(ç w;Ðj[`d¼>NΩÞÜ¡«AÒù”°µv7ΰcÖ BT éAäÆÛ§yo%óðÝë‡tðÎMÚg»¼Óàd±qB5¢p4“ÄqÎö|6xÿ.%h—A$Áªr ˆ…Ç=U‹Ñ¿Š³ÏÙøwe2‰T¸ŒéÕÕ˜z¯ztyžÒå;îljpcšùÃ@øä4qb  µÑð©Ómq`^gT‚Õ£’íICÊ>ðòEBgϼ~ÊÏPæ€ewo—ž¿x*|ì"ähENƒåŒÎ^œ '¸öWKµ³óE¨ÉiÌNiðå :««~nóèÈ ¤~xu Q7SÓÅÝæ…UÀ›·Âln^á—OÙ9rÅš%ôqæR¹Ù)WÁéMkØjJI+ŽŽ>{ñ€flƼŽl€Dll;UÌZc<)6@.̸€îõ&;q-[¯Ñà‰iÙr#cÄ=ëZ‡úg|€ù¯lUß Cã\A»‚éêÚõë&¼_߳ɒ.þôÏ© =°^ДóR§&JPȤØÕ2ÖQØ2RT7&:NBÝ2ªC7šUz4ä{_zõz§üK¯É¦¦áœ–ŸL/Ù8ž³ADvÏ ‚€R`Óv·…µ Aœ9è,XÈýCeÈÏèr:W 2gÁè¨H&Eâ5 <ÖÆsÊà[ËMרP# ø%ʼn×UH¥ÝQñu_æRjfGPŠèV{%¥rØï)gIKv€SvPËE(Fϼ8\Ë`I=ç`ÍÛ@Ý'šÑžó0†j ¤cicØ‚Zsœ¯çàS3‚)í~mÒ²©-%èjÓ“ÀñOÄØ31n~{˜Ûlä..FìÐcãÔSªù˜qµéù…#eweÎ3j¾ŽI¶¢vUO€‰µ¼kB•A0ŸkÙM©ÕŒè`wI·–dßæìÄ Œn3þÛ3 ûèüEÎ{©P‚JiL9Ь¬nE éålå«Ä’ÜSµ˜Œƒ£HÚí­*Ÿ¼þÚçe>Nd+€Í{vxY£­XÎ,JÑÒ‡/!àÉ„˜±³‡àà1=6¹ITÍ&CÌÇ2҈¯Ĵ{=Y[K¶  oŸwƒÆ¾^²Ê¦Ê3îoŽX‚(V!»5¾‡®@F£½ð¼Uâ´¢pþ³ŠÚX°j9‘e]NyO.uôÕâ=àóõ/,É„yW•­°·Ræ³îÓèìœÂFŸÆµO…) 4«öe…ößÞ¥Í1¿õÿ5ȭذ˳så9H»%±Š,MÊݨüL¸;lØÁ‚7L{4zz¥}Ý WÿÐ>@Ï7ŠuRÁŽ ?}ò)=öOÈá Æ]ñs.±ø´äëÄsh$PReÇz|L_ôAî.GŒ±-¤1ˆ)½²% €pN`›†H£øˆÈÅ*Ò2[y™ÌrÝìt£§VNÙYqÄd!9ˆ•XEØÌø55ŽþGÁ”vn¾Kç“¿Ð…¥ê—v^B·Xx¸·G¿œIܵci5yЗæ}¾äì{ÉIÑb9!·ÂOq¡Â=Ò@.r©R;àäßË¥–Ø3öñ¹Ñ¥/ñ^„Í ŸíWØÞÖvé?üÍòsåËà¿á‹®ú^\iöœ±íãs ´ÙþòùwE‰H«z:éaS¯Íf[-øw±§¤`xî¸e•~ñ£s„™l×Vb[ZÎ5\HE&X Iæg“*å®eqІB˜1ì•V-qæ‹…ð‹ z4Zð/PÆÖyÔ2Mç3)±…ó¹\dÞ]SÚ-ÄHÚ$ðO@0À6r<úŒ õÌ­PÍÌKʘ“C¡AéÕ%†2Ðã Úá(e_w %9סE²¢«ýtM$*  Ã8%àš£Ù,+6ÑW'ÌWÆ™oöý«l(¾ÍÙΟ=é‹€Ì^£< Aņˆž>{Bï¿ûžôí 8„ªÃ¢µǪf§Â~4ç5qT¼`TÏú ;㨖×5@4k$çóg”ÀVůéV=‰µ9½Û«s R«ÑŸ³3° «Ö†îzfÖ.1ãd>;Ì¿S«ÈxØål!׫Ô4rt •k¦öV­*7ßmÙtëÀ¦Ÿ<>¥Þp®å$é»2BD¦¼ž5®Tip0áçoÓí[Gf>œÖÜWü9LßžéàÅÑ €Ï6™ž'’±'‘=Óæ_çlé˾Gg3Mé‘ý`.ßµ Ç9âXm3®#Œg¶¸CÍŠ–Àº—MnüMœX «ë,”  }uW€AÀ3ĩÒ¡QÝ …–{î„-MH5øy…ì´Ê¥„q*Œm/ûzè$#×î(èP8_ZÐÖÞ6gS9seÿ&•²Sê_\Q`…ÍZ($A{ü3»%.¨ÔÎÄ)V¿ü>½RǸ@ù¸U JØ"#^¢£Š¶¶ذRB‹€c#Ë[Ñáq‹ÿÛ㬩J>»!ì €”l'6´Ôet~Šx hWƒI”b§ìˆÀu4ZmñÞ^-Pp¨Ùâà–—çª·2í"U0“ùZƒÄ6\ƒ´œÇbP=ß6·jI{)¯êåãbF œß_ÛBkÊÖÒÈŽé'ŸT­œ*Ÿ][¸v´(ÓgìMNL špþì„ÏOH»>ÕÛm ¯¦4¿*a l9¿æ;å£NkYG¥˜”Þ²_vàeÎP›%Õ·F6¾5!(?\Ⱦcþµ”he¬$S]°!ÈP®6H5&¶0ò-åQvй>KϳÁÏÌ„"›à¿å½)Ÿ›QáÉå òÆÍëÒ+ÉÑÔ ·àü(HIr„xdÀX9},T¢0âòd¼”x®e¸´­Bï eàÝE ±äïqM«r•Fƒ+ v‘ÑŒ¼)Í…ŽCxOÔC_’ß¶Óäh½Û¢Ý-ÚßݦÎöµ[mên5ÉÛéЕãÓgœá^rôm1"•åd©d×¥ÙÝ•ÜMÌ,t>Z“Ô¥Ù‹ON ‘LN…ª€ƒdƾˆ0ê´àã R‹36½¨Øîº…”š>øôyQ9peϵeŽÞg„.oÕÏ £ 2w Öõ®ÂøÎbŒ&Â_¯X"¶ƒ¹hš¥TŸ³3)ó³t—”<8ã€rL½ÁRƳž¾´8P¬Êu;›ô¤ÆÁIy‘…ëÔÝv¨Óæ³WçýPãÏA÷‡Zs»!<Ý(gbF#[ãѪ(#ÚNΖ¶f|Œ8ë)Æ"œ {òÃèÁܨa$³,T¹žøàÈa|0¾ç‹òÜ2Ue¡…cÇz pÍóV‰ÔL“‰ã<ì lÑŠQrŽà6 ù3©YiŒN.“‘ –ap;uº~ç:mïíSck‹*u>{n•*Û˜4Jtù”ŸÁ0“’¿ÈÔ„–!Ày]ãNÂ![¥Ì×½…{lÈœ½Ú’d„l¬Æ¼/N^ èÉ/Ô{:§`k>[‹;¡¼mïñÛ©P¶BàÊö’ÓÑ9;©ÅÕBÚ ©gîJÕG‚VÇd³â̼U‰}šÙc©‚Á{¯øZx~¼7nÜ¥fg[UËì²ÐòÖ÷ØÁT\çòßJÑ[*ÔÀ(SÅ.´8‰ˆ\mn’­¼rn œŽ!˜ÝxÏy|¸î*'q¾Æ N°3+€‰1îŠÔ40Üôç˜ïklDu…}‚MÕ´.ûJ”Ã’]<þìÁÉ ÇüV+¹^ŒxB›í+ØácÿQns ðJUϬX–Ud_{ÎJìÕ6Õ«-þYY‚Øgô¨(K%Ç#[(Ùz­¹Íûv['`бíTq–FêT¦Að•…21‘®´b ÁЄà5Gàì”1Ñ‚³áÑÎÖ®¬Ùl5à÷‹„x'\J€…6îîmmmsïˆßr¾Ûª}”G´)ѸN¡ÄèéX¥HŒ‚¢Ò¯VÈ/ûœ9V¨ÁNy½Y8{ Yn8r•³Z]G¨h'J€’÷Åq0fAdò•ÔÌ8‚ÈýÐ1?øúN–KCކRU±9z»B5É3_w–DìXZò>–ùJV{ lK7&Ôr‡žHïp]*ÅèØœ?» ç`¢Ï 65ÆðPŽ„HR”™‘^q¾äkDIxÀiÿ£ÆÁxAí­6-ùÐ|ïc~¾Eº¦y=i+SQ ^ã´Û¡!ÚŸ]Ì9[*­gaÇdÈZ.ФδOïܽFõ_y‹ÜYDsSÉ T¥sÔyXh”ïsö^…f.¢ ±¶¼Nœ–Åüú³ÉJ\.«\ôœý‡C23zÌUΰ¦ËXZ®!¿1I…p»ÃƒÜŵ•ÙiCáYZΑrzFRÆŠd>ÔÞPa ÎDp‰[€‚Š=–%â8C3CŸŠ–qZµ¤2ê—Æ^ ±’ÏDÅh•Ž!¥FïÉÛm[œrªmCG‘èÍ{¤ˆìbØÛ¦‹‹1;ßHŒF†PQ¼‡U.LJ>]l™ù,hmÁ±íAOOG0}!„.®*c-‚¹8p´#°n“ùŠ.•úS²hõrNÓU™ƒ/‡í.}ù÷î*¦Â5c8&hÎ|~Ï%&ø{q®åu+¿ïîxôÖ{{T©¤R‡±i6ËtÉûpj9Ý)ƤlÓ‡V~z‡ ^ (sµ¤žå}d*}u[­s°¸ÛuùZÊÍ•b’pçy‰:Û©ö”ÊD) ‹rã\´À›‹“Í9¼“‚A5¬“,)<>곫¤#ÊÂxýþmé;¢\ç&J[(W–ÐÆ©ñúdoEsCF;Ê÷hÑ’Ñ)IA%‹BWÃö©zèÉXÔèdÊÂ{dÍF ê?èâ‹)£D‚%r q8Å´Á×Ýâûã ήàœx|¦9cK”Lùó'2Q­m“43A›]œ }8 ΃vK*(ózÒ- Ûù€Ön•©ÒvE½XRU(ó¹ôå59T³L­@Q¨u”Ée­—…ŠEÎ4ÐåcŠÈˆ'ÙBÇùBÈz¶¨½}ÀB•\´¸"$CðØ-~ áU”¾p¿ÿ ŸÏÿÝö8ÀåçTJ9èˆêÅ,úøêÍÆ—”,BZÍQJ³L­ýpå³ýák^ND¡Èâ/€.³l.#«¸^Oúú–J‰Ý(˜×$ø™rð<á‹\¦È‹c™8 fc™NØ>z‹ƒÀ2þŠ)§\ï)TÀH0V)¿ŠÌ4ÈR‚UPC 8ž±OµÛB”ÝsëÕ²àVj¥‚Kaª´ËhA¸3I+~ƒƒmöŒ¿{ãè#Èäáˆb™²9hÀÛV ÍKWÒ7vl•œ^ …ÚµfˆÑ¯ðœák­@ä`ÆLȼŸ-óÉ#vâÓbTG%>àY”|K"|ñC˜Æ™ÎãTËg§Õ¢–’aIÂ&÷ù}šî:sÚdLË6~–šÙ÷ÈÌ’î·jt>å Cz;Vá„r  2Ìÿ¡ä‰þ Ï"ÀÅ|JœHQ(ct|1®Ãs2™SƒŒÕMÀô8ÆõL•ʲUÌÄb£Jÿç÷¿$K`Ü" Ê{‚y6.’…Á”<Þ¤-~À×Þ»FñÙˆˆ¥š$Ùš¦2ço—]:ZÄ)GüøâTsÆ×xÄ2Ú'³·‘óìÇÒW² u¤-à)è*°ëðŸišÇ(¡sæXR”x JD(›—uÜXJ×ÑrB2`Áƒ3¹dC—Îuɤø»Ó N>(Õ8ŽgÀ‘ZjE?8£ÏÝFfl*2#'* ˜ {ˆ37rš‹}Äêø3ŸÏÃéáÚ tkÊ‚… „–£äüš‡O½ Q€˜ $Á™Ì޳Ñzûþ5Úß­ÒçOmHÉ[Ëì–HÞf ‹|"äËuÎè¦|_Kã<3A vüïñA‡zý€–±Jæ†läž<_ñ¹È³XGÁ^Ä'!pä|øJ,—-uä‰f«ÄŽôÌxU&NµÄYæÎŽ:‰W/CÑnÀ©¶`–éÅc{õœe®€ ÕrfÉñl¡N¶½åÓán¶; ™6qK¾´·b‘ú\  ÿbSVicá7 =E–§2Rty5çÊG€Ó®ê\)±Ì…),áiGeªÌvŸöníñ™­ÈZ€à#C™8LEÐ"±bE“…9‘$¯Šr»%åÿh¡¸ ÇPðâïáÀ’ÛðÊg;8d'ÅŸVDŠ÷&—J…ã$¤tnI+ –œÅ ‡AÇ¢º ·Æç~Üà$azÆŽ9ÒªÖ˜h夲SÓçÉß#¯Ü¹¤pÂ^?ƒß-êÔ¨îɬµÇÂî{:zoWDP‰"A\ôÆbÛP )xöðS¿w)óÖk®úHPà‹T¹ ò1×I_æû›ð~“9;S€)J ³lf‹„÷ô@煮)3ž)±Ë~„’#ßOƒÿ{š­Èë"e:æçƒ½…cAËãÛ¦­"¾(rÙ×ù:"â“r–^*Ųb ø2i¢A¼¦dH0Éûr©6gN)¶W”yœQ¦œïÓ÷ÿî½÷:¼ñ¦¬/²™“W1—þeÿ dÀß.ÓA@ÍÇ ¾^þ<«Ê“Çö+£ùe‰<Ëã䪡$BŽr; P-Û5à{üöYA¯è­1‹Fã'Ç"T$Ž£8%;À¨$ª½íÉÆŠ%{²6ø²SŠÆ#û€2®jƺ¼fÛ–Óõ­T£µë(¦¾×håœ&Ÿ 3ϫД=sö)ƒ–¡H¬EÄ–õ`o×ðº›viY·f­y¿7÷¦3ÏÿML©.d‡‹gD©¢y%2T®j°—âÈap¦Ó‰\}‰³o€™ôálLvØ©säˆ2à¹K$ÔH¯Þ”Ÿó2e‰Î|0¥x55#zî­n¯Ñê‚P0¤R<£-Žânߨ¥óÞŒfáªàpß$™Á÷¶e­’%åýl¾§á˜Æ"ç:X*æ ýR:X[Ú%¥’UV8(Qª©~`Ÿù®ŽÒH·šƒdÝÝfE*m6èe×^s÷˜š¼” 9x¸Ä<¿´WQ©áCËq–5•¼ÅÕ2wОB>J­s‘¡Eö„¾W¼1?ŸQi¦¦óòùTœªtó¨E7olÑV·ÎeYZhÁ|%ÿ";}Êçȯ„69ÿtf€àJOFB¥ÌVbÀ€=0Gdøä¹›™aÕw( à´Ø hûFWßÏ @ÚT+1™Ã4;•%Qáb&Ì‚ªÌ÷WÓ×Çs« ¿‘3"v’Ä4šx1¿Ç¨B•E“üIK@€Ü^²½Ìøùùœ±“ðlÃ<çØëD Ó£pk%ynp®Û—1ø"•0 YS¦Žô©sv6Ûè‚ç#•~âsPÒ(Š]ŽBˆ­Ô©sÜ¡îÝÞîÒ÷]+wéFç˜4¢ÅdÆ÷áIÏ}“6ÏæÕù¹á›Ðñ+`4V åOX&ú R‘Àõ êcÁSÉ™ˆ©êÕ¨åw(”é söxšY8¦2TZFùÔ20.Û¿¦ÎAµ½¤ X5cºñ{£z,V4œ O¯À7¼­ô¡e AíyÆCÆgÀI¡eQ2I+8.8€³š\ ‚ ÿd[½E{ïÓíûߦw¾õºvûï×-Z¡zÎ<Ç÷Twö8hjë3äõ\†ó‚ãÄvÑwë’àŠUqº¢Üè¹¾hÌãâhþ%Ÿ!$ŽK ’\8MÙ3U‹L‚»<'ç·÷>’ECö)yÊx…ƒd%©[Y…ó³ €É1õ’³i39Åo¼ì¤ï—ϲâµAòÓ#´³µ`JÎkl«CÇg-8LMtF:3'B+|Ó(q GYAVœÉK |mÓ뀷M'žf¯;óØ|°oŒÓqÀ‘¥¢^SJõ·TuØ¡%, B #D)±ÖŠVYC¶fÕzC¸Û~¸BbƒÖéË‘NíC˜$&õgôh–™RrÎÛ¢²Uá °\Óߤ}„¶»xöœÌ .8°¼’r=8jN[Ze­zG˜×BŒ¼­æúL*ŽØ‡?#)ªi–8–/êa LT(—tãè†Øƒy0Ñ9”ÊBGëª`?›$b“çLzW¹ÇÓ\ùfCáu©½¶Ád^P«:Z];R#S)üÄüÿ²­¿_IP‘°@ñCÉVܰK @/‡Ci6Ò£ D¾†ÖŒˆì$F•xcÀùå׊ý=ä‹iÛ9jù—¹i3•†r^YàWÜÞnÒùlAϦ3jX+^l[ mÌ×’Š—/_q†qMú僠ní“ÈŽ^™1GÎ6GMŽH\¢$Æ‹!¤À†Fx¨lÄ= SÎÆÑƒi˜ñ!C…jf°sÝ&!$ˆòžùŒðéù%y•-ŒLkÎVµ)¾!oãç…¢òÞÕDQÿF>ï¸éÑ3ŠÐã›ÚÌvŒeã8ªFTá åð½A!Oö¢ŒáÈ÷; jmSv¸MV£m'"k®£ãKXGÇ|Î’‡_Zɾ‘©¨XÚ¬r{‹ž<í›Q“X*ªwþ:dÿ¨M•j™¬XÇÿÎð¾ý-ºö9›aç—!ËÖ¼c1GÙ~*#6;[šÌ8Ї–wLÒ7—·æÓ¬IO5üŒ¾äµ®FDÜâÌwS$D´Ùš-/– Ñ Y Ƴ°¬Ç·ºRJ÷kuÂ\—‚ñŒ¡´Ã:>öéÞ›úù§3ÙçŠmp¥< å0Ç Åð4êûR©ˆV’¢ÈÐÀrÕn!Ûhbõd‘P²¾qO[*—W3qˆÂHgçÜN¡Ô—ßCµãÒáM8A­`¤Ôôi]ǧþŘžÿèR2Gð'舖‡™V(B‰Ø(ý%ä˜O‰0 fÕÙáÇ_Ž€«BA oÅì˜= 9 Ÿ K—KX€ÄÇÜÛ¥|æF‰?2c„è…W,•äÅuðÞ Ç‚rP@åIF5ë6UߨÒòJô„¶JÔ(¹;–@;ž5iâŒÈ«ƒÜ3ÒR?ªFV"jdëè¥c±Úà,ÞÁPjYôìLc¶5Û|Ö£*5w$cO$ËçL»Ñæsx… ¶¢eå¹ZA-q@X+ù’Ĭ2P‹Bôªdl<¬žITªuaeË’*_K(´Ó5èd²ÇÖ=6¯ÈRò£ÔN4cOËŠ3@ùãÑqE"B\w¯ß‘ÊL°@å ´å1:‹Jï”×ɓЮþ<ƒÃ‘Ò~iÎÁJJ£!*!ª5ŠÔå|Háì„Îq°7ëRÀ‰D,Y¾rZØ)Û“l›ƒî+V‡ÑªvhÆk„M†d"r–ä%hð¹vÇä­|­z&¡ÌœÛìEÙHRËk×9pÚ¦üí÷…ů”Tz˜T+£ˆIÄ×ýÛG{Y›JK´æÐvrÚÖ¯d´â(1’u9(”²Pxn ÁqøPêÆ’ì+‘å“>7­ßœ°"/;æàæònxÝøêª³XŸE¯i†š,¹Vñ_sÔpæKô8ÛwÖ`¬×fËÍ=6uú¾•¯CFÇl”^ç‚Î÷ù¡ç룯‹ì¼ÀòðK2’6MeÓç\õ¹<«’Ž8"œØsÌ'´+¼.…™GWù÷9£óáy9KhÈŽ_¨ ®èÉ© *Í:£`FÍx(À•Båï*5Aó+Á„Shò¢7mhD‡ì8ÏGsê—ÔŸ„2–eÆ1!~`#6ã…CV®mQ>ìi$ëY~ÛÜ{£žŠ«CA×›‘Î…[P/ó8÷iÿ`‹-õ9ûM!RA•'k–´8çìv.¢‰$:^ rZ'-,IAdDöŠñ.¹RTô :T÷"ÚëÖh‡‡ãÃ-6‘°:¹v&“]6xww=Ún¸t°Å†ÅG´ž â³Êeaγ €hÝFÂ,­ciÛHø„$„ŸS`q Mî¡ýàØâÄ­×ή÷îÝ#e£œxFyî\¯Ðàb¦´¥t, Ø¥}ô+q£ì‡àȱµJQîPVp  ŸfÊW­ò‡îUnFE¹ íœh"å½,Ó¹z€wÞù`—3~þÌ’M”s-‰ PŸLIÙ{)d+ÏŸ‡B“ÓÛ ‰¾k/$Ãjµ›Ì/¥’Ñíò>8lÒ{Ô<ÜŒ…ê‡Ã²°Ñ²æ´à€æü|B/1'®ô¹¥M§eµ¶ü6ï¹Z&Ó2Jb~ËÆ?¿(£ÑÅH‘ïË\$Ë2csö†j˜K“Á‚âR*`À$§h5‚)0¨LÄüÍàìJ#KGÛ}@[ž"áÓ%¤8ëÔî4©»‡J‚'Õ%ÙŸ‘‚0#A¼§’¤Hða•¹x-½n1¢RÎÖ³kwø§ó1Í9YŠ8XAŸHo‰ä¿÷kU:¸uCÀzË‹E¡Ø¶iSSž£ÒRœ¶€;98«WâµØv¨6ÀÜM.göO'OhôŒƒÇ1zǹxI 4 Ç4µêb”®±KÆ AK[®²Û©ÐtÒG•_ö¼d¶¦ÌÿF áRÀ¬|æFKr£² `¢Â‘`01A§dãÚJ…NCµ¬zÈêÝfBiŒâ¼©¶;gî¡m艕•3¯ä E*öAZ“J&?2.çœ5+ÅÀ¹KuØp†p©ehÃMFŽì¹dÑtœJ¥WgKþyº¢fÓ§áàÍF—쟞ó¿çÔ€ˆ »Ûš2cÊ.²%èœ-FâÄa?=PsF.cƒØYÕ Í Pflÿ¢K ·ED:ç/´RDT'Ÿ3Wƒ¸ô+Â(©9fi¶ž ßtæ™vÜ9“›Š1U:òpcž<çZF@)©œ<‘hÔa™3Pa.;]pÇ.°Q©T©ÃNòòâ¢@Ò ò8‡6cófZ#/Bjóû¢G¸™‰¢g?2(qg=^+‘8œxÅôÒ+&ÛÃÏ\ó;|̯ßÞ§ûå)ËÈ”Œ,™ïö1?ÈzxqF >T~µÊ›l´fÃ3}+]÷ÀóQ7±ëÇsÔv ”;6­öÔÈ‘ÒÆìx>3‡‡`¨âË.¤úz“€&å©éÅ*”‘&Ö¿×Y §xµI€|¦ÿu‡3Ÿ%ÿ;„‚Ì£¢„ÃŽ§ÂVå]ã™/³KÔbúæ!¯E¨›§Â-ã/dâôµ=e°Bd¶ª„y.¡lÎF‰žmÙ‚’œhÎhg‚¡Ì²‹™oQÀâ@ãî?‹ê>•ë¾D§È`‘ŽÆ Ù_Ð=ߪ‘Œ©¡/ŒÃÙ®»Òî˜÷3¹ÉÞÎ ù$P [«%š‘“(×YT•½6Uk[ì ‡TæLk{¯Ëƪ"ìIðû^RÞ«qIÎY0³EzÖõrpÛzÊAdeQÚtѱ¥¾¦Œ–dŸœ%‚€€_³¤ê0"6-F–¼¥DÔ» d£¤ •ù­–ÊU^²Ä^¥Ü©67É©Ùú[Ñê)0Ö¤Îç¯éR»R§Ðäã~–LÀzdã9©–ëMEK⟒ä0ßžO`8¦ê–®”í  Œ¼Í{ìgT?³(Ó¬~ŸÁk]Ûåý¦Ô~ƒ³½§S™­–6'ž2+Nê ‘N¨Ìò¹{|IöqLݬE‘½¤W?»¤YoÉ ï¹ÔQÛ`ˆj‚$”€úliÙØ—9l‹"¹ÅϸvÅYy4,p %~®ÈšS¾ptTœÛ“P@oã`DM«ÅÛ¦fÚ©™ÄQ™RT<ñ¦åŠž]E¢+9¯ m•Öê ì8í¤e*DŠçq$!ÔD®‚]SQ ´ÓØT+qM>[©,!`–ÑÆåR²ò|” ÈòVÏÁÈÉ®f´{pƒ.ÏóE¦Œ_N#yjÚÆÐLˆ9ÛF€ž9S¨^i°ÍšJ0{¡èÁ)G6ßSÚ2G´’Þ;MºyëHz>ƒÁ… ÓœR*Ȉ†§’T»ÙWȸ¬¯”ŸíóߣTrI»œÑ ýñTQëN¡_l‰Z¢8&ôaP†ò† >dI\”U܈ ±ßj“?¸â%2Je:SŠr½k¹B‚r÷”£Ævó—dUçüg'v-³G&HiZêÄËyÉZ™×ãiøØKôþA‹~òjÀF ³~¶á#W¾xŸDʇƒV+ÓûMŠÑ;9ô&(Ñ iÍk™…ËgtsÛ%Ž×u‹yñàVDí.E+*±!ºŒ-qÖ[ì¯8³™<ëQ£^5%iE]çðÁÌÌÞ‡†ùtsì.ËÉQ@ ”DBàr½ÉÁÅ"‘¾/î¥ ™QδÜfɬ·m(XzËOt»ãȘ!žM†š0PÜ-¿ÃŒy¥³4”¬\ÜáöæHÔý’ÈÀNÁê–@Šs=CŸ ÛS¾O O4g¬Ûì”â!mq]ÐõEO• å8LQyl#›¨Žmï­7ac¦ª€Ò«ËçÅ2ƒ¬Ïl•ð5­‘æD €ç·XÆÄÎ$C\8 p}È$+pH‡×vTáÎÒþ:z§M~Nàt W)Ý»½EŸó.U¥¤¬}mGæ­Q¡È¥CaɰA•Å–,Ar’X„(öZ5m£‚±+ |bƒèv:Å? ³)‰ŠÒ(Z]¦‚–dÄ;Ü¥@3—£C»û%j>9ág«a]éç—ÄÀ`„0£íÎ5:=ãl{Õâ œ–¨Ú!±±*W&b%4ËÆ4‹'ü–´Rœ¢¤n™ê‹Æë`ÿA´Q–± tÝd,Vf½ŒÏä¸7ÔQwYßsäŠÑÒdÞppЕÉ » ù%ßh“„©ÐôìDð„D®R³œ£|Ê@v^b‚^‹ªS¥æ¶#£B9_z®áŽ=½Z¢‘BÑÕgäz[´a€Â¥öØã„ðÞ®›èÌ:©\&D7´§eƒÄ”ÛÕŽ èÓrLõ{ üt 4®®iKxî< $|«LS€ÏøÃ!ï]v Î,#Ž /ë y?ŸùClAð{ÎzÄKdKj··y{2ÿlÛ‘®°4¼wkû{4z1,JßÀbÄMy½Ë™ûóÕBúØIcI£+¾Ç&ꂃóÌ÷¢·#íÇ9ìD->ßN³$Ù¶´PçHÑŠ£f‡䄸Ӓ:pŽ,I¥Í¡M¾&¡k&'gG kšAKƒ_g#ÐÚ|$\&*§mÆví²0ÝŒŒ‚eRÆeêNyþÙL@Ö%‘DÖtà6dà­ G\¨HàY6ëLOóµâ£LÀqTTëÜã 4¬)×=ÿw½]¥ùbG4 ü²M'gÏ50stBÁEBa„óymÓŒ×VÆn6øÇ­¯Œ½ÖC7Qÿ|¾`ƒËÌžeúšö†óÏ#[8ÞýN‹Z@Äó"—=;ý»g' L‚È<¯ô^Ué;8ù(g*7º6ç’%€5 ePi:IªÎ xijQ‹¥W¾™ÉÊ\!¿íÿGLj§ÔÁ8fkà‘ldÑ­Á`”©ã¬°uë îìP§Q1¥#Mƒ¶5ª v¸à €£4HÜ¥kå\¢Uœu’ÔXëÒšõúˆmdâ9Ïxf ÷9@,aGÞŸGBñŠ€d’Š. ¿;—Ë…Û´šM)õ¥YV¶,8â­sYçbHPH‰JC“+ò¨¸g~¨Mß–l;4ÜÚæèAøcСŽ!¤À\ðçŽÛœ±{ض˜¿eã}1¢Ø/Ó¬U“h;ístú‚ƒv>'ç3ýLŒ¬ðšb4î"°$Ë_ë„o´R(+Ðàe;”©ËA&‘~½ê ŠŽD5:žbʦ/8 ´|8+jðg¡Ý\€lÐ’eŽdâOøóç¡Í†0ÃdÇ=~v!‡QJ…üOÀ†¤RÊËéŠp¾ÿÖ>ÕÑÓÃâ¨IE‹Aµ$Ì¿òÕëÏAš+÷m™m”<Dî–R©\-–AîZÿ‚'ÁÄ)òq»mÏËÑ–UÔ4 ÇH^ªcXÈ®–‹X©L§‚Ö„ŽÐ†Ó2†\i¸-¾'îÝmÒx%Y€e2r [Êâ¬Vâ,Ñ_ÞÝ»Æg~W ]ÊõðU9šÉ „ÈÍŒ3ç(Z%s/v!mšI0c‘ËF4æ=Œu¶>Ÿ @lñUŒH927n‰AÂxªS’•{±Œõ(`R-†d©nTd*R…YéYÉ òAÞOh|RKþ@×8zë4=ÉÄójÖ=1¸a£ñH2ï}uÛHʺÅ̸nñ³O–êÐáø€Z^òzôÈyìe¯¢8Ž`Ì¢ã=K­Š0Å!‚QîÜìÐüÙŒ4úÀ¡Œ®H™*ÀyÀv´‚<°cPæ,•iÒÄ£ÉóÑ9;€¨Œ Œø=Þ/aʺu[{:î&µ`K#©•%ÏÞomñºT)XÌ ØTgùS̲C¬È¯ÒËÉ¥X7“uŠWHBà…WJva”Å‚êœ,¸{>Y­29UOl»µü~ó¾ŠL–Õ)Ôœ´QHÌ [¤Ì{"j…u• ,æ”ýPéÙà`Ìò:´œ 9[nq09–Àoi²r”°Ðà™´ZlÑ 5)±¡)Ž…„ “-¼ÿQÁƒ.þ9ïÑØœ>vìhžyï8>Ûà@ZUüIf<Ü-ÈÐvÆUj´ª´ý€Î~>•3;02r٤Λü>QR‹Ðîòkô›ò[ÜŒ4#/2nÓ+Úe›ŽÝDžúW¼±“ÂQ.¥P¶.MçŽFêx·+}XdÁHlóâÜã¬ñçã)9øæðMF™–×­œ÷ÒÌMcÃõ-Ú]ª¡°®¥¢ö•ڪ׊’|œÓ„£¶ÎW²òÍ2;\ü6zá™·ý•àÅ2}Q|ÖŒ7æ¿ýzSF0Zå™v"1°ŽAZ‘fïK˜]ÈÇ„3Q>ƒž—?m»È’ Msƒ¶ÜdÖ+¾/žU^€Ue༴Žn'!S¢³E©mµº¤-èåòÁÌ3üÞ·3Coš)`%2" ÷xVül¿æÒó¯1FE2™Û·¤–Êa•¾µ:‚¼åg§sz{šÉltVsêFWÔñurð#÷4_вÓ`4§ˆ£ß zå¢î ð.d Ü_›3ã9‡"ÂSQuÛ®`LŒ³w6õЩ³Çì9YY£ÐUÈ:ãÀ9 ļ±[æ¬<PŸªƒ£%v¢ðŒÅ!SŸ'"/ØÃƒá\G¶Ð²”/Á>2íüœ´š>¯{]Œ‚½vMÀ²Ù@R¡ç‚ƒÊè5ý‚«À2Rˆ@†G¡Š·9)RmLwØ/à@ûye3ð¶‘´6DdÐwS:™âHE ã, P`2pgõúa°ÖƒŠ0úþQ•fôâ9v8²rO'X0‰b+¦LPå*µ¥”Øëé«ÍkGè—ç2²ˆu„ÁU´¨ñ¾V|åäA™¶ƒ)‘FÒ_GÔ"ŠU6¸&þê:„ãA‚Ýlñž²ãŸé蜋ƒg‹›zYþŠ=³E©X&«´æù‘Øêma"ºá²wyïloCÔ‰³°¥/ùnŽÎÎ¥Msº`ÈË@öÜjf+¨×ÈRæUÇŒŠé/IKQ©¯w;¥ùDe“Å)óïZ€V.]²ë¶[œ]£]Àg…´E×y·KgÿþŒŸÈJp2BÆ×halŽïÙ]•éh¹EgÓtÎŽ¼Év‚÷×),÷—•ù ‚ ‡ã`J§‹þpBÛí}JAL3³aß …"éZiuD‘Pæ÷Á"èD¾KmÍÊ :|M™JÝ.5»×鬯3•3½.©¾U¢kß¾&ÎFÍ6-+á”ç½SÙ{‡Ú{6Í®¦4ê?‘Yü(éq€7—ìºÂI2äÌ´umÏw—7L(¾¥âmQš€ !ÿ}ÄÙï¤g&r"¡E ì –Ë¿+ç à`«ÄÏ«ZNcWPñÂæhçɪŽ$Çfä9–gçPMfë•¿ÏD&%"kÁï§ê”àȽ¬Bo½÷œò=­¸ ë¨ÝÚ§?üÿ‰_{Lžý‰Ê±ÿx\’t7ÏFc“™™µ¶4_ïCe2¡3ƒÙºPYþ¹QksVYJã˜@å¦÷w·èÓÉÜpªs»’‘Ç2ò Udɱî:¾èA¿p²ÈÌÁkìÊfW zá£åŠFŽssFŽÛ7+s£öº—°íµ´S¤$Ú»ð:ðçTj}­™ÊàÄS™‡ MÆlf”y}<à ‹ÒïŠÖØ.‚¥³´iƒ¼(‘R¡ZUp‚›é8¦x¨„gª¥[× פ:ÚÛºäkowÚ• ê×H‘„?|%ìp™Ü£€CÌxÏKœâ0ÕùkÎVâó~(ú  ÑÍÜØ.”›°$ÏçÚ™-9³Žh²âl‹F™³®–¯ã6u»š.h bœ~Ïxh+b ¤ÏFÉ«Sɯèè‘É6×­í÷Â`îrÔ=]À°’€ÕàÃQæFEßågÕ§ï±Ðgh •T6²Úˆ¶êõ'];qI9f¼é—–•1S^õmzò|(£dè+[¦—ŒõÃ5 ÷[¹}»+S»¦Ax™  *×qÇ=\\¡šãNÜ3ýá8ZæªD²ñÉtRP¢æ3ÂN.ûrdæü<"у¶ÖÎ\Xø<²IÐcÜ”2 ž=‘„=ëíQ­¦ë[Ê6FR¬×ùäsD·%÷`ÓÝ{M:9Y¨XgáÐf§tÉÁG›ŠV>DåiU¾òþ¥–c½«XÕô­g\ƒ“ @beE. FÃé Ç€ú)Æm¨r&·Pçç0¡g/i éJ^3ãt¦°Â¤"[iáÀ‰™R¯ðù»Ê}€·'ìÅÔ¸Fz\‹_¶d—NÙ–¹ø½ƒ*½s[’އ_<¥ð9;òêZYΑ`)3šì1Õv8ÛÅ\9?gö]R=É8û‚ÒmPÜ"n5KB:”A¡«—aÚP¦ÃÑ ˆvÞå ÈæÔ8l‘SS2ù4Uç[»Ù¦òÁ˜¯æ´´C•Eaȳ®¢=pD%`è±-I”]¢e½®ä7%ÏŒÔQN¯(%€úd{$†"Z|•w¤T]ÙÙ!ëüÄ8rÎ4ù¾ä{àOøÿ·wŽ)nÔ¨wõ‚‡ Wè`'œ€ÿúÿþGôÁܡӿø”z?Å{k©Õ>Òq]ج L…BwœŒ8ë¾euJÌNyIËk!“¸X†¶%󣣊|MA¨½€%9@_UŒoëK6_aÛÚp|¿¿¿/D.V:–D&³| (ðØ1JkFŠLá4ÓEñ‹IËô¡ü=¥f½EýÁKÙ£gö úä©#A¬ëaWèуgÔŸN%¹Mb¾'k‡þø÷þWj·‰ž¾x)¤9ŠXOi«Û?¼Ÿ;ËÖ¥qü[¶Ö³â´t³Lé°?Õq%)m˜Ÿ/ ÿ¹•RzÖV›Æ‘=Ë,1s8}§áÓã1@”×ãØX$2Ã(HU+ÓH_ú~ìÀÚÛDÃKí¡ Ÿ)á@&ÀˆŒZŽ s\ñï¯8ó-l¤Ô‹±2-±#Ó³Ö#wÊïkFLrµ¨ÉT²[Ë­²¡¨(šÔôï=!úƒ¤\®Qí[Ð=’é‹;†ƒ~iŒ”ýZùÓ*@J›óô¹3¶­\í̼Ö¸ŽÄÌ‰ç ªœôÆÚ˜Ç×`0R§ £ÎÝ‹†-Í]¹ ãh5µ +äß5Ë©ÖæØ,£¹‘8Y(¾ápÛ&ðÈØH<áŒâWö „4˜€J7¤3;÷–¬Õ ~ŸÇ¼±ÙzmW4ò•R+xƒ=¿¸W+ç0 ![AzîœæNBe]zÇÌú‚íÎ?¿@@Q \ƒ ¥'*ãeœNmyt>V€œû‘…“šì2-˜¢fœ½Ñô’³ã°à¸N´­¦I¥>ŠºGõZ•vžHáJÐAJ“È0ðôÉ\G­ KžnÉPЦb¢eÌ™80b’è\Ç̈«’›NÜ&Ò• šÜ’Yp·é©Üªdâš/Ä)W*ŽœÞÁh%mÏ´ MöëS*_¹c‡©½2;s ðÜ¥-Ž Ñ ï÷Ä` *Ö‹C‡^<…Œ¤šœr=¡F[0õ´˜£×Ç¿÷M†šå#ƒyù|Eƒá”&Ã9Mù¡g®Te@&#x>ËÕ4×€† [¢x½¹&+·í¸8k(”@ñ 3ãF›¥¨‚†3êÜ,ÓÿüÒönÎgôÓŸ¼ rÖâ3à®5´íœ ^q +v‹pAv-¹Ô*¨gͳnjz«½K/¿ß“–TõØÊ@0d)?û 70Šb: +,ôæàßg›êTJ’éŠ~’ŸD+¡Ý_Ý£—ÿ÷^oÎôJ™díÈ´½ÛtN=݇‰uõ ¸ø[ˆØÔJJjd‚e9o‰–ÖSß-ŒI–odA+]tžåù>xæ;õ8ëÇçÔ¥.ì‡*¥P1.Dgü<Õµÿå×é·ÿ—?¦mÚ¡ñâ &Ó sñ™€>¡+á hz<aXSÆþ°¿0)Á× l(bciàï\hÈžv¥oí—x?¦CÅðçg23'ƒÿR"=j£Q7”É%ê² yøð¡p«—DïqE˘“Œl!xňäSW–<¥M˜}(g#Ó1?€X«å'J!±’J‘½¤ï½Äåý Ь³¢W¯PUa›Xñ©V¹G'Ï^ÒÏ£G•*¸zü5ÒÙq¶G-p#Œs'nÙÒ”ŸÝì<ÿÆlFPòÊ Ë²à6Ï^§BÅ÷ZMz.àD·MíÞ2ï Ôð7w·éËɉ"î úkVÎþf"«å½àE¨7Z´ùR-‘‰OÓB¥ Ä*;l¨ÏÙg^™ö+ž&þ¦!â¯2‹Ä€!7æ~WùZ8Ú( ÌxW­-™?VÄ~ÅÆa›ïO„K‹bÎQó‚ÿ{ deNC»I e ·ýšs¶6œøšO7§>ëaø0çP¹Ñ¶ôÐnÀ{$·_U!“Ò?ü^¤Ò¥žTèpãÕ0)Æ3ØÒÅbN·8 †¨Êœ?¯Ž ˆç;µ¬ŸÅ ¾q„ûh0§ª‹¬ä2±”Àmk-:ÿrŠg:²ž)Fñøµ™›ˆoND´.«gBÖº½¦EÎ4Qž…#– iRÕÊø^š ïmq†Pi=ª\¸Û€÷2Z›ïx’Cþ¾ ØU²€<ô­È:Å⨜ËåÅ3³ï¯øFª¼ïþóßùÝ|ûõû3Q—rliÉmÈ^œigå,i2v¢—°ó›ÈÝFË#Ôl¶9h6_¨ÄbªsË‹P÷ð¢Ñ½B¯½Bõº_à ðž¥rC‚dç‰)U^qV¾³s @µlão*ÐæãYëäÐd:£v‹èüÜö³yàŠ²X³µûy1-"²¿3YGÊA‹l@ƒçK£•-ÝnÕ°Våýq2 ÊTÖ®y!ûcB%”ºÙ0Wª®ÐDGRA;Elæ3|­»ÃñÌ/•øFy,Žª·¡šA¨”¦£µw³œž€šÛýò5ú§7CÀM)ТîÒgΈ[æ´ó^¾mbeTVD¥ÎRð˜Åœ«Œ"s@âvò;.-®4 ÎyÃ3qÇý ïåšNY Ì-$©Úa£Ó¦TƒŽeÎy¬(숓 Þ+­k[TŽöªHwgg—v·h:p¦¾ xìJQt l‹ ð¤”Õ-]Ïr¥ò90›èËg©~¼ÁÕ)_¯Ý¤åé9gá± ¦ÀA¡¼Îwôëçÿ]oÖ&Yv]‡í;Fܘ#çʪêªêS7%’²%}ŸùäùÝoþ!ö?ò“_lê“DH S“k®Ê12æáŽÞkís#¢ŠPHdV÷ž{Î×^kÊ I .ŠaCÎ?{$Õi ϾüJ®z/%xýJæ—]¦’÷í;â'Êènôä°­K 0‹‡þ µš¤îG0;Gq”Òô"‚z5‘ñ8ƒŒqV´ÕªÒ­ Vl „>¤vŠe§˜–º¾fÕ0ÙSª×ЉŸ%¨€×ÏØ+÷뽄 ›¼Ã±ÑŸ‰î X8­(çÒïÈ%FÕôD5+ÞX´D‹g.óëˆSq+’¯Ÿý^î.¿’Ѥ£I€ô±xÞ,’¨mÓ'lÕ:M‘ê=dz%ÿ\þßÝmÆÓ7JêøÐn~|Êö4ínV©3Ízì[¿ÿ@ÛÇjl¾ž­™•‡PÄQê9Çä;½¥tE©x¤›|l³°¦¶@¯E2ÝP˲ä¬÷±fƒÇ^.ÇÍŽFPFcYîQÁ‚²Ã-ï½þ¿Ô•‰ŠœÎº¤’êAš-Ç18e¸H G"ÓÉÌäDG6î¥+’ƒÀi³ºC^YfNýgôÍ+o«2f²’ëKšÀ‰çF/ê9ylÀ­¶4ªI[zƒCê¼ßÜÝIŠ!ž“B”qÊ{Vy–YU%r¥Œ•e™Cn“ ,'ÐÊÚ["È‚#€ï÷[ò\³¡u9¹ÎlK‰ˆYçÂõzñúß,:ê*F¨Äœ„V9ÎkÝøSMaḀ¤¾[—åáyÎSÝî‘›§«‘Ë{euRêÚ>xð Î7Žœè­pF̓àSa ó¼“eA‚˜Ä@Í& `Ü—ã¥k†3šmø{~š±ô°â†6ò¬ö&‡Òkj^“™£Afã:]–òïÿâ‰üïÿ×ÿ"Þ ”ÕÀJºÿõàßΖN:×Ïœ‘×k/B=ÐþÖq!HÃXI[ “ä-ò5ïkä-Êü@ÒvZ™üé§ÇÒ†ÈFÞS½þ¿þ/l±€)®rCàdó À^v¨>Ç,k`D(W7Ky¼Þh–ذ ¬ö×{™y-SŸíÅ|ê20OÎÎByúlMÖµ(BfÙ$«[ƒ bàÁ26KÉŽüÄw²£¾8„:DRv³2;x1å0PËB;ÀÉŽï©QëûÍnSÀÉ L_±ÍâÙX“^øÙMy±ËfiãE@„§Ô†IGBO±dykAQÿØ“óG-ù7ß?•?9'ÀÌ ùÁã¾üŸÿÇ¿ÿ¯¾ëJi ñ’‘,Û«‘j®Sž!ß±íaïýî›´“d¼ÕeõÜýNáèA÷X-â[ÿÕHzZȵZ]VCÀ™ÞNºr4Éhšq>¤%3™<ÆÁ¡‡-öBk4:‚Ü·—39>mnË ïÓúÖ •+ͦ Ë|p©r b¶éÄûÝPNŽšòÿÓŸËOúký]cŸ2'n€/Ó,04‚\Ël«í„±ŒáKf`xßË È¶•Þ–3ðqbY-¤C7™·.DÏãá½¶¼øbâömn¬dAáìÆv:²Ä#W›vxÈÙ‡ÕÚÈ3y+§³Ñ½©¡€\¨¹~ºäÈd$kÁ\?)¤YõÊè{}€ª’.+‚»9‚˜îf®öòTí”ÍÈ×I€É€rl âDa>€ çïäWr jønNÇgÅ Pûξž˜ü§ø.a‹oË|JÛ[LWãõbíäøÃÊ'΂ÓCîŽÿ=£ä‰î ¼1BçˆQa€è8Q³Åƒªå:] èõºR^ß©ÓÔý‡òºfç93W½*R¬tD•Þ[ªI¹$^½–v±¬PòÖ'wÃt68îÙŽF0±§„›ôðÀ)Œûêœ/ÞUÃ#Èï7Óß;·‘Y·ï¹Áê²:õ"–ºN‘µ© © t>PmE@2žëkÅMÙ, œ· Ç|=.I¤¼®c•×¥eß^ðÎ@7® ,oHšÁ@ÚÍžLVKžEÐÑæ…•öÓ(‰ž/uò`èïl**(Ø^‚»&¾K¿>:ìmßÃ{ß‘Ë{Â"žsæ•MõE›âúmNR †>ête s=ŸmË }˜˜y­0±\Ø3=]¬‡]ùr4¡ I;iiÖÉH7 J_a„~¹qŒ8yŽÍM\ô[ên\ŽtA†Ð.Œ,áñ°'ÿþãÇò½{ÇRh†±œ%ÜÍäVï:P£™éµ­É½3¡z’~´tSö±© ¨Q#íõ½z¥bÄäY0$Ȧ’®ú ¡n#N‹íF_ÿû-_†IƒÙ×i·eLLPmÓlê.Ób¹fv¹ÑÃûR3©%œŠ¾ÎÍܨ טÄAëu»¶¡]`Øp=]§Ñ³f!SA¯±%°Béëttí‡C 5*'ïqàzà…£a­97Àáp(——·¦Äùú]ôVìqЧë•üèÞüüù¬tÑßCP‡™iT”‚!pBT.K£ÖúðªÊj‡Ì¯ŒÐ'$L¸á¼JN%­ž·-‹‰·SÜ+ÑÆ8#Ú¨7 ›qwBzÛ@1+vB1x{Ç]ÙL‚²%SË¡·`ËÀØUG0Ÿ”æÈIKVàÜ6²²ÖµäQ+–ùª”?ýx(ÿÛ_ž‹¯kb)²`wÿh+Ä! CPú[ ÃjSÈÕ¨$A J3>}Ýïkcµ"òÄ)~ÎÀ§ÆcT{Ï+˜‡¼*»Lzo§'M¹¤|® z~¾­Êξ£®l·‡z?†fÆ«øõ›±üÉgGæÄ‹­Bé¶?»¶(Õ0õ½!F&lÕ|á38…,a@ºq~ú‘Ü^¿[Ñõ*t_@µ­r•ÏgÄwã›Þ(i`S¿Ö=r@.¡ƒ(ÜX0¯ø»‚ºÛ û@û®¡¿»X–ŽZ:²ž/þ¦­dã,­Sa*ãç$ѽ‘hRrPr$r~€b.o'ž 3 ;j<ÕÀ¾üúVn_!Åq¶gg³ÝV¹B2JÑNW¹eÀó̵Ë)%ybm.”T£$" ­ 5i±•EﻹiKSgågü[Við¼ûš@›;Ðö*ÏY$«/Òíõ M­¦ýüÞ}yþô¥„»÷9óТhø,…Óa#)íjOx%4Zf\/ØéŠå\’X-Íf«| ‡å”ä=Àš@D¥Ä{§/ùåˆ÷•®3õ öêQqòAœ£ï!WgT µ<”FR*/êGŠÄ§Ø’”5© X}™­®--ܤ Gg¾®ó[Úñ²ÖEwÕ¹ ˜¬$zêlhp!SlþÄUün éï™ìòÉñ±¼|ñ‚·4æ5D°­[]ýÖHæéDÊ•‘úàl6›]Úe ˜l[µ®b\•{,%ÙŽeÐ:—™få„0Ñg¾¢ìq*ShZc÷†Ç¬¸¥öì$ì3J금§²XE*NŒ¯\¨E@æ{Yt’0¸Ošp¯² ÃaŒß©_»ú'èä'¯.Ô±™%5i0ãÁÈW–dÚHѼ¢Hʼnÿúþ‰üÕÇO¤uïDS5*z»«žüYg,?Ó õj–ÊÍÍ­Ü-–ÌŽ·Jgz ,­nè“Èߦ(¿`kR‡ BèšòÒ¯ÚËÓé’e|2aékçaÏi^;´oUnðg©Ie¤…5Ñ<[nKæÈìÕ}‡¹üîékî@x΃=ð"`0E^áÊà( }:£õ\#Á‚sÌݨƒƒ!…Zàø®VÂÁP¾‡DÎ*§æ8?}¦§gÇr}ýv'óê€cÞ^™4­‡Q©X"ôgu›ntƒæ~ôCyþü¹ŒF·ÛHÔscÞvüÎecÎùìï»-óïo…2¬5cÍè¼0µ¯{G¡\ÍŠ­³ /xYífŸkU5Tð¼ÒñŒïgåYc`ª÷eV‰C1 3?yŽ¡•ìƒeEDCÙ€Üj•ÈÃþDò¶O~õ§—K9¸¿–àVpge ì“- Ÿ7Bë7Âh~ñåLŸ…­sýU(s®"T”¼ »bÉhŒÙ?_YißÇÞÄxZf ÿŽò ÁÌfÆ!…Z¯ÿÆïB:ˆ4¿ŒPŽÛ=q‰ ±Ëë©çüvýñv•[<‘ÉtÂy^rf< ÙfB9ûxèQåøèH~ÿÅo¹¦Øè]‘M.V¾aZ<“Šô¶m%×— Þm1l ¤òzJmwlÆ^/¤b–ÉøÚGªÎO­u¶ZW¦X™ã‡£öÁ½Ž\<3 Nˆa ÛÕ1ÅMœŒc]Åb£gJäò[dNwRÍtﵦ‰jz]¹¹œÊøe¦RÓV9BáÓ‚¿aX™F7î¦#w7;¦Ç}zèåz!Q«©)xJë­ÖG¤‘t5jÉbÝ/ k+•(£½’íÎðpšpÊ¡Üþâ­>s+©?¡Ÿ›šEVÝ9×ܦÖÀ©ó\9Õ´ØP¿&P6&„ƒä¥Õéh`ë‘׳MëÍ­´’ž¼Ä0žã¼FCÏ,ì7°‚†&QÍDJA¦D‡ È:LÕ5Yëû·V—Rè5‚²úTíøfãøÎM_x.Ǭ;ÚGxÖõU…Y)V?¢½Ù0(Dk£ÌM¦˜ì˜…£‹r¢¿Û¶d„DwM×¶€Yjò¦¾-8àžƒ/+¨K ï¡ñTƒÕ" LVÖkò¿uÇÄÍ+ŒNUOò± {nêÊœ¸]m±]AàÐЈ ªAÕv~ï\~ûÛ"'ÏCòQºª‚þñPæ£ó·àÌ?RZ¯¿ôwìèDz¯VdƒãX #> } ýNBÁ’ªfƒž\¨Ã<<9ÚΫT{¥u¼&2WE±ý>8lÛ çx“:ÚÖ‚JPp^¤¤+Œ¡Jܨ}K£”ƒkùD3ñïô¥¥°h@ ƒ®½ëbÎßÏ4Ëy‰²Y°ßá—† `)ZbøaK÷úúú¦+³E%ß¾Q8¼–sd\'mY‚¿`4—j2U¬Q62ö¨}RÊ>{1ø­Ó^Uˆ ·:ã†àHUX}Ø#»W%GÇ@ì®ÜƒT§M¾ø½çŠr`KŽÞôlf²­T³‹KÝÈâFfC Úz-ë­¨ ²á¯4Û<==°àaK+¼E«,¹•ÙtMêP,0Ô{W«€ãk‡ý€2­} ÓÉX6¿axPj cDp¥Î|û®tkŽÛ<×¢‘=¸•w­Ï’£Ñ´5Cå§ÜãG‡ о)é8} œàþkfCð(´†úì¿ÔL`·4'r=.¯äÅ7iªMjô4xk–ëËó˜±êRƒÜ<V%ž#o)ÙÈ|ö® *¼Msaepãqåéj©¶S¯ûÂp  ñ¬($¦K b#SYæ“0ê'дa¿ÙÓŒ¬ä|³¾Ù£L™l4Piy‰øN‹üÑȺq'E¤¿«{ïn~'Y’ŠŸ¨ƒ?jËàÉY]Íeq­‰•&W@†¡4çMi |]Ùh#6œ)wáP²¦1ì:"pN O¼š[»4±žÚÈj±Þ²¼An4[¨h[5Žhxé‡FM j¼îCf¦ÀP„ÎiZ]H9í1ûò\! w“ÐÕ÷tx‹*'²½*œìµzëåQ{´Èk`É‚bÊÌՉߒò¿TûŽdª{¬Ð€d³Ìd9Éx®Ê i*oÊ–.Z&\¹~|E:ã q¨AÌxK3æB’ZN\ï-·“— “¤Á<«W¯^qÒÃ÷lj¬´!¨ l>¦P·Œ¨  ç¦Ø†‰áñãï,øÍx¼GòÑidÜ×kÄ6Ï܈M¸ax0|G Ûw·Tí—l=ÇÑþHòÍÛkΕףh^ýÁ0Ìæó Ì1ÀÌøÑ$å’¥]êW(g"²óF›‡š-¥£A™¦«—3§›ìyäD§º›#?Yã^ô*;Þÿ~ cú;9?}Mô+8˽1Ã{ØJä&O¥£×xÞmȯM±.öˆ]öiQ]¦iÑya2 ‡-ùíõŒ}ûåt:r«N KDOª¦w]늦ŒqJfÈkfœnâÀwÙš«dìóÏç•‘…ÁÎožœÈóWowA†súøï 6"h„ÏÀõ…ä+ ÖE×ûÇ?üüâóßÊbi™$gD‹q&;Ò+ƒ»‰'þá;,€Wíæåk4^]b+9£Op[G+ŸA9ÌîÑIPKÓ©UAö¯Ûní]gâåSaUÙDFK éH|[w}¦@&£t}Ð*idI[IàZDÙEÍ‘åµfi¿Ñ¨÷n¾‘Ö7!%²Þ1™¿¦Öy­ øæj­ë¢ÐiªãѦAÌŽQüJ±]ÜjÅWœÙÇ kÁ, ~>Ç…¨_Pí²gâ%úúÞ“L“ë/c–3ÞÁ#M-DØÕ5»aÙ>p“EW©+*+Yãä9™iV{À&2c ÏX?{n%€öúÑnuäôìžüìg?§zIArÍgsV°ŒEƒ^uä‹…•q ° |Ç©¿ëÍRî±Ù°j„¾JÝ+â‡ú‰žëVGòNÈŒ2]Îäåó7̈°.hgl§_ˆœ×3ªö|ŒyFΊ, ¾" JØ[i°´¶I‹AxÊÞ~9Õçñ²)ë(—©®Q\†ÖŸÐ)Ü•ÕIMªï×D"20B¸ÎéxnÌj«ˆÊŠÚlbÌm¤F»‡ûÛœâ®|^²2·Á¼®fh§}©ÐŸœWöŒ6- b\¼š“DÃtä­žf¹ßëÈWO¿¤Ó\B.v3ÑàÊç½4³Nš]i>éËàþ}yöfôÔ@ªòÀ¡Ñ çÓç¨t† üJºA²Fêý¯¡/ZL48X{¢˜ºòÝn,‚‰®ÅGßÿX~þæïm6![ §ñêLŽV†Ûñ':rÝð³ÅÆxÅ3[Ž&V‘•šlEùcý»;G“ëúë:æø°q1Ý[=¸‘cgÖªbÉŒI»#íã{rùü%q;Óû °XY$0rrÛ–ñ³Tgú×W2ìt4HY»`•è‘/˜¯VW<ïÍf‡ÁíááÁ‚ãÉ-¯¯(Fú1T[¡o4圼µ¥"bD@Tw~tì3±†ïƒ›è7eG] ÂÌ9rå®:Ušê =ƒP‰FS•±J¯k‚é®UœU¸þ¸ãǰì\vzŇê\{jÔ¦pâø€þ4˜ÓîêSø¦ºÕð Éï %_͘á£7h6Hä!Í s)ýÀseîÊ©O¤í4&3»aæT‡¡eŠ¿¯dÙÐÍîšA"û¶> ę́ópÈ:jD&úzŸöÒ¢e¥zw|‡šßÅ»Yñ>[^åæïõ 1«†P Ø„0¶…ê„Zu‚#Q5ë]`Î.Õ§,Ñß;twjïùÕ?Ÿ@Àþ®’¦V×I½søöгÞåÈ ™8”á0Û¸ŠIo 炬kùÑã‡å ñ#‚—dËXÒô·$8»Ïž'[Äþn†þ½Ñ;Ù©ÚÕ¥ud·#c´sk ÎÊ``#ƒDôGF@…¹n›=÷©Ó=_nè˜öuò½Ê”ކ Z§êøÔ@¡wê›ôgYYéAHæuÈhÖ /åv¹–§oÐûΈ Eæÿáÿ e4T û]¨ÁþòÛ•^GËeœ¡ÑÃÈ(- òÃq:XÉ›qŒQ¯}í¹0ø§ç¶rök¶5‰Ð‰fZß>›9$´•ÕÃj7VhFž—ÆÙ8žxd‹¿ÿâF~ô®}múåÆÊ6”~5êÞ’¸üw{‹×‹äüÁƒIß¿.77×[Äð|–ÉÉÍpŽd\œÜmôõR‰šš!Jý]*fu¯œ`+7ùÐÖ€óÕ¾c’ü§?þHî}д èÜì;Îrk˜HwÖ‘ÅzÆÑ%‘¾1!ú"ÔöN+_eÌÊ}8öÀÊ”h6ÑZqãXಹN¡½“[ý¹ïô¦Žd\Ó±äÙ@­¢P ôË¥,·˜øªÎÈ«÷ÔÍV.(G?àöîŽ=üz¶õŸ/8׬üŸÞN)æÃå£,Z:ƒêÛŒ%)yÔlÊ–¬æ#ùà¼É¾7l ÎyèrÒJþX÷tçrÐ0B&aÛ;9–¯/¶À±.)¤ð¶ÓB +$?»¸¾¡ÚõÑGiÿìgŒ*{QIã1Þ¬d­Ï æ£gåï»¶NÝõÎ}ÇDöŽS¯¿vN= m ¬Þ[¥«(€Ì$-öÔû*ã>¯ÅLP1úÝïמ*\,äÕãM. qÔ™Ø:(ãÇšUø 2ÁhHj@’¬p GP‘.3Œª”r7]Óa€é @³é—/Ôy³Ý0 •Çš…v:5‘ˆÏ€_cFû €¼ã~&—дÓ?&ƒT#" &…º"!¨ìÙsϪøùà@`Ãg gOGá °De>Ñ¿˜ƒòàM¼¢®`<1—Ã1ÙçÈ#ÍIŒœ÷P‹å•$ùæM$N1B0¸Æxä½{÷tü¡G¹Fn(G G¯î? 8Z…ªMÒ¯ˆþÕßMäù×KYMs–RPÕ‹šñv¤ËŠm›åWg:eÒ껵)}2€‹5 œÔV-Øs†GÏ—+¨VA£«Î±­ë’ê=ûF6•¦FqÄ¥«@˜ìïNŒÆÞ­VèÀ—喅ш „£iáa(Ñ!”Ñd«ÔØFàö:”L=[lˆ{n-ÝÍúûÅ…¸¹±+0L=j˜†”ï«™­Ô6Ì¥éul*ŵÖštÈTWúa$¡$}6ÈçŸn¶.Ë™{ÍZÞÑ·>|ÍJ†l| «¦±j…i‚Ž11Æ-¡Tˆé =hSµ«Ã£c a“aƒc«°aö[ƒ†:ÁºÙzƒþPÔQþæ×ŸKt¨¤ã‚Κc† êÀÉέðl…€8%x·914™ŒœØ”UØŒÊ5p{ÔxÙK|Ã=AÎx¶z-KdáÁ†ÂM 1K<þDÚ½>÷äèP¾þú‚ê$oËaOùt¡6bí%äé^hçSÏOIÖ0ÊZ\O³òµYip³™Ë Û#£Î/F.§³+MvV²P”À‘7éLŠr].;ýía¿)×Ë­sðÓ4NOظ#Q¾ÕÌh~‹:³-äíe&g'+*v€}–àk·¶:åü¨¡:¶ûÑgóÁÈÛ·o¹°'õD (tkH³m@ ñº)—f~ô?žÊzúFžN Ó¹cc¸s,~Æ!d%pÌÏçà÷îš­A˜Hž‰ùj·¤wÜÓ¬{ÌêANÕ0»\'wqˆô>g’£óóšµå'2žmlÙÖÛð¼`Ǥè[ÄimG@©á@˜•cR@£â`Plʬént4Ó;r‚åyýdf«2C;æùHIŵŠRKmßÕ\¿¶:ÑÛ©ø]hš™R…¯W ¹ÿ£Ž4ÔqÆÉC¿"…“]½[Ž¥l:)ÖܤlAÔå9ð,l6t™¯¶¶™uC8[O pD˜täÝî«ëeJÞþ²Zóü{š±7ÔgÄIË2Hýý<”gOŸ“ ¦ÒçžEKYÂ\ÍØCɇáv.pÎ|D¿^ïùéé5Îô5ÜèÏËod<¹6\‰³¤AÙ r_x-…õÈñ÷aÉùöIú–`·\zØ1Ù鈈ж[¯4ˆ=ÔÄonóïFa?á9=œÊ"-…‹žJ×òõMY„„2î¼3­ÏoSBS %!fÞuWŵ^ó´“3%%ú¼?~ û·kmœ Z}ŽJŸ 5K¯ÞêÙЬlë:BÑ1VÈ)' {–W+êÉ»¹¿Õ¾Þg?CV¾˜Möz–f\ —Zc *Y×sÃ:èuM\c½"rt[»÷Lß»Ü)uîz°ÞNJ³­îÇÞüþêFbˆ¢26æ²Àf¡Ó‹²R㎛ ,¹!cµÊÏuãNÕ`šºÉ0G«÷3Y¯å²wèÛj†ÑS«>!½gÉr\å¦Vdò`¸ƒ¾4C‹Cªƒ{¿0Wé²dlö}ß =yõöRzùí•Ë©¸¦Ïxœ¿ç¸«w3cRÂLÖ0§„µ|¤y;öyPu#^¢Žý*¦¶A$'2še.“ö]Fà4DÞ)­ïO /üIà¸ð|¥@ùGÜÆÝÈYQ£Í¬•v€ògGzƒcùüW»-å"‚†ÀÈy+•~iÕœŒjëÂgÿo…ª‡·ŠñC7àS­ÍìoO”Ll¶¤:hïô_Fór[Ý=ªoø,©—®oGŠG¸X®¥U[Ž}#²…ÊBú“1¸À·?[‡ò³Ï¯ôà Ÿ[YU„û°¤Gœ×G¤*z´â¦Ý=•Öp w«Œ{oµ¬å:&fßE0µÒ³Bö,fc){g@^³ÏH¦Š$D³…C ðEz=3ÉÐ&wØDÃ"xv²ÜïÉÛ«•Óa6¡߯Üt…)s5šm¢ºó|·!ðì_¿Ó˳ªS€˜ˆßTÇØQ'~¨™ô˻ذNŸºýå¯~½¥)F‚–³˜Â¤Q±u±îK˦::lÊíÅRÖk£ÖÍX˜oåÀaG‡(‘Yf[ùµ€Ž‘á3‚³£ó¡L0CÁY×sZ‡a}Û–ú’¬Aüˆ%t5òË™Qz¦ìO <é\óa]r÷]E)¯¼«&¥ë\¢¡GUµš@§^Sdga„RêœWŽËbWp!ãÎuáÚlŸ@;Ýk£FF.øâ²’ä̈Wî4q9<8b¼©d¨Ï²¬À aÝ;;WÇù”Á@“\³ÉÐÈr4â‚55)¶> ñŽ ›Ö« ‹ÒЊ‰0û¸Î·²6‚Ï—Ïži"§Ïz¥× 1Êôû‰ãƒ‡ŒhHÂ#:µÑõÞ£:~1 šÒ¬Xß×Ó¿÷ô,×NœkíÆZ›í„,š¾n ç,—¦[ ÷zïø‘,^ë^ÙÐVEÂ~>6CÍxYˆXPTËÓÍ×o&rÚ ¤qÒæO¡1£ õÜŽ4!ĺ›Dšà×¥†‚&0q[V«™ .­bB²êÕ ¡‘/©ËÖµQ;Ô6*Óu]Èx|¡û­Mb'ì1”Ô¿ùö+¶QÒõFZ^¡6Ó‹J£ÕuúÔU0Cßb¥yì3T¨æ\ "Pûì°ûïÊì÷¿ãXÊžæ†õÂ+†Áv4¨rÚ.µÇÂÅÏ@ä+¢‚(+Ȳ˜jL×Ûq˜ïë1ÔbK÷s´ŽŽz-¹›5d¤¯¢Ç`ü* G2ô¯(& (ïR÷´Ñ’T#€ïnb§ÄÄQŽœ#Ô1×ÍÐñmÜïpÚTŪxecRA񾆦•v×$A6ŽÞ8VÅyE|47 ·ïh±–Bz³Ó6ídW¹X;ýï÷÷þ×p¾óÔœ#½¶O:ò÷o'4òFTr׎ «á;žmÌp"[©,{ƒ3‹êÞ;Øë"ctÛ'ù¨ß{¼¶8ÊçØ–}ÝÇ÷‡ò|sG.kÜ/æßaWÇ•häô~öé§òÕWOe¦ÁR¸†áˆË%uÉq݆'/¦•<›‚Q JeÅ=²Ü¬³Òæã+×2©Ë온gàkNÔW“JŽ\«ÌŒ&G°6è¹ygý8Ô£t{á• JjzUþg¼xƵŽüù)èM#f—©ŸI˜ÎaY_Ûè,͘£=ƒqï=µ0pÜABpv ÎªV–ÃÿjE:V¯XÂ.¥§åâjiä'µD6G½`8r²½î1 t¢Ðž­‰ÿ¸u—š{gÇî( ’‘™=–l«‘ŒÊ›¤RkL`Í–êlnF¡!Ïœ. \ÛcEèxXÉxä©AKd0ˆl=ôY•Ñlœ-'Ǽ×êšS17WÑkn4¦›0}e- ?ÀŒzáÈQ*fƒEnåÃV«oÌ{{Ä…“Z-ª8™æð¬/×ÏFšeVD]“Le«#i’ er´5ŽŽÎÕľâ¤è7××al{Þ;½¦L¶9ëZ÷ƉZºMÏøE\ï†{ObàÀ·H’HÔãq* "÷ºÇ‘ríöBaŸÁ~˜†sZ[Py½Ž"‰5»š±ýØÓûŒåù_¿"elÔ´jãp8dF e?7 Y…+sØEX«yî&F*+—r,ΕF«ZDÉɱ‰Ýg¹¶Å~)­h?1ÏPc•“ÀU_$MÝW’°yžüs]˾LÆS]Èäõ×’^NX&FA XŒÑ·Ê¦ ü“‘ 2 á’ Ë Ú$œÈÛ7_J™®ôw‹í¨§8ºì*Cðꚃ—Rƒ%Ç(ØÔìvÐÞ!ÞÒSÃW-uÿRURIP|@\Jépô÷ȳ×_9b# ,KŽ˜åÖŽ`DmÖ<Œ+’Ýpî¦í5àØÜÈr¹Ðó{ûªÝnJ¿ß‘çϾaû®Zëu%¦7áùÇz –ÌŽÔ°¿°©±5¡L7¦ ¹µåz½û%Odsõ†F”ÀTTæs:÷Å9f¼œ3^8nöÑ0£yÁèoeøš³½Ø^qïÌÒMëû{Gj¸>Ö¬ü¿½¹¦AÿºvìƒýEÇKF7T4SLý‰¬—KÍÂ4 @Uéè`K3¾b³Ð¤bèAƪUô,ͦ¼š®M´Þ±žážÑ7‚G&”©ð“’ÈànR­ëõÕ•||°m= ¤¾,ö´Üÿ;¥uü“5ØÅeǹn̳žF‚—3ÎÜ£|ëSªäXH’¸.©zuÒs˜‹–"÷Æ=ôh3y·PîÞn׿ìÏbÓiG–t| ·7#êfÕFÍpÛ·Â9;=#ùËËWoLœ (éø"=Íœ÷TûÊÍ:«1 Ôª|ÐªÆ¡í¥¼4ï4[Ù|7~h0Å «ÜÔt¡ ¡ÊY¦[5ÈÄ×ùô–»^9pÐþné"Lg+"ì&)j?J·Û4¸›Ê€„êã{-yz¹"aÐÓˆ†á|}"æK}àªÜõý1\á×¢:nôÎÆN¬’eŽÞw{Hâ•¿qŽÔ„í’ìtï ØPšW#SúÔÆµê@úóij#œµP|Ô e8åê&5!·(ªXV†5ðÀNØ–X±.f™;ë DIbùô;‘Ü?må=ž¤A}ù祱íïw:=uˆ‡ò+ÍÆÏDavÕNxTØÊ—ÎrT¦‡¾VámHR‚¶^’ª ›c‡þðÑGrs5’ñí˜{c¥{»wr"¬~kTÐît¥ÈröF¾ø"g5‰ 9Æ9?¦öv €Q‚k¶ Žôl.4HšhfZ”–tµÛпïPå>Áv°Ó'hƒŠ&0 €mÙ:}«®Þ\ðp£ÙtÐÝŽ¸ÁI¢ºÅ%+È ½íäPn£g5é ÀÕ½ž:þ>mÂj=Q»=–ÑÝ$«sãýä“å›o¾¤ÂÛF“ÀŽ6 Œõ|p v(Û G…9û ˆÝøµùÇÐ?Ñ5e÷ÚÙlzøòå ]¦¬çKiàBõAc¾T¢]̪éƒë0ºÒÊ”Îð27ŽÞGO/#GMÏx² ×g¯%No‡V?ðíóºÚÍïSž÷Û-éëûÎSËa¿¦µò:À^ÝFvsþ„BÐnõs‹ƒ”]ÖK²~3’4˨óëbÜiDuú²ÌsÇ¢ÊëÑFÏMWR7Ïf´©ný¿Êqªk^èAÑůÖTTÃa¸¼¸”³³{ê´J~gSÈ;Hò÷ge]ßM/˜n»þ´—èõª} Ö£ÂæÉaÍÀ£m<ßÕ–O³Ü«,•D  áÆEMû Ü"·¼_ºª²wTM"þ‚<Þ`(åf­N|)EÅžhÛ·B ó½ï}WþðåD”Â1PúSßÿ´±’Nd$XÖÈ`~ºdAÌcÀ²ö\•Âõpbp£záÚ6¾Óålƒûþae Rè×Þ@ ¤aãepä¨*¡^·‚Ê…:Û n™kä°o`å½ÊÙ¹\E ݘOßæÌ ¥jõm,%IäüÞ)gÓ)÷¤qc;^j'´àyÞVc¾¢ØRz;Ú\PÄŠæPö%31f½Ê¥ã&b#8jÔx4~¢Á‰Ã±åNõ*ð÷ð'úÞÔ–ëÛÌ6¹^aP8yS Ž£ §çv¬ŽÑ²yØr8¨“ãDþÅgGòÃw.×79ƒ–ë+dô-Ênú„»{òàáC¹¾¾a‹ #jy>×õ‰M$à  r9ÜBîÁjÙñ¤ ‡êL³a¬~$vñ+S@$š¯Á›¬”Âõo:fL@•º²Ø1Qgyøà@Þ~u-a ¢)‰îÄ9që4õºÉ?ýÓÔ¯†\d”h°×¼“%ÿ•ÇÞ.ÕÊ‹å×Ð1¡¹Œ”¡¨ä€¨)êÚ³¦š€¿‹”9þY8ªï<0á¥†Ç Ûf™šÔl/f0Ë6pÊ£¢ëv ’‹œ?RŠ}=F9[0ó?ÌÕ‰wí\’¯¿AÐáíõˆÏw¥ f¸q^½<´*ĉ RUZ_‰× Ü‘ÚܪFÏ‘­þç6 y™ùNÈÊ+ ¼+(O©‰R Âô6@H£½ßéËñÁ±üæóßð}ÖKT:Î¥¼góÔäZMð©]ý;iÆÐsÄWŒ$ôƒ•™Ò³R³òbV¹wü¡,æ "þÛí•ôÚ ¢ä}³Ír=Ú©›ÕÆX-©¶›Ê2XºÙlßUZ'ýŒL¾-˜è4BæR´Z 8ÆDé‹ Ê+:c#¤4BŸ0²éб¶ÔðΗ)+ýž)ìu:m¶'ã[ʨB'~1GòóPüz)Ý2}Öðu Õña=–>iÝ·zF.­Gï>¿ÿüÅ̹<çƒûOF)p£7(d=q õ±«‘.å>¹Ç¡•) ÃðQ]ìN åì!h@õóÆb7Ÿ\¸ê}È|嘽žôä—oo$ؤ¹Z‡3óI9Iвßßê«ÒHµ:ÛGF„sÅ ~©×ù-ÐÌúÀOü\&ºñRDe®']æ:_B)œ#Á 8I(¶œêÖGo&Ôç }8w£õ[YßÊ\£àƒ~›÷˜íi¼±瓸Ö÷>>Õu=p}5¾·`¬*L• }*̃ ŸN€³å–’Î3c3îòÝœ8Ý™:‚s«®ÔÙ×q[PÞ÷Al™9E÷010èËò.à5GÄŒˆHìrÞzËy¾AÕ³ðu_µÖQß}]£ðe;bWsm£œüè^(7·ómݸrÜû ô40êµ+½†68ûáúÐP-)êvIµ+9£Dýà¡>³_O5»¶@g¦á¯¤ ò *Öçæ©SÀdF¬†gCZȃ~ ßý°+}ÐeõWÿ8—ƒFA[rpp ¿øÅX‰¦ñƒ/¿Û#yÆÏös‡¸ÆØášAÊèöNŽ, [[ŤÞ÷5åk]ªB¿Lb0fxÞqlA*bVÑˇÁž÷JV[Æ#Ý¿BÚݺ²²µõúÃÓ¡\¿ÉìR á¢-­®ÓÐLïääž¼UÛ²X¤xÉ4ЛÅWÒ¾ŸJC Õl¬Aà¸à¨MÝîÜ„A<×#Ç3Ž5Ñh3Ëõ¬ä”\ÓÊ{7HÇ(Á™…1»"!80DjËŠ×zþa³n ðµOA»–Ö3IÛçûM³•ìdW¹øÃûåųF0³Ée\NÀ¡`nº ­ü^©Sk@<ÇI= M 7ÊQ9E3ʇv½‘7EdÏ‚îdcªdÀу–ð<êžÃ¬³ÞïÃûÈ‹§/týr*µ]_ÝIvo)xÙs"œ¾my ëˆ9×Ü×6Wî©·ÉÖê!(‚LµHÍUÁ‹'‡÷erñé¬Q9 ´?æpòHÓŒ ð ¨ï”¥_9¬ÏN}и 2éúÿ³á²ZØì7þŸ8p3þhÝn踅¬…?Ž®Yºç&‹&³\êYè¶"¶ð øÃïË/ùKÒ¬€ÙKK˜0ØYª}Ét­çxVžAf³Ò÷8p#¦në{&¨A½Üèdžþ)®æìDdÃØ`N•3‘ÝàgD|Wœ+¿Þr™BŸ×²qfš >züX~õëßXp\Y-‹–Ÿ1 AéÎx™–NüÂÛ¡aõyBj¶š&<#ÞÊ$AÅ ›=Ò²Ú– Ùß-9ìN‡»ÜTNëÚÀm Â{zÞ»’´Ó¥+F}Z½[ö¬™Kü¼Ù å¾ëúi“44«hI™ÄF‹©ÑÍ«28`ìŸoÖ[•5¯ZËÓúµš”W—ß}7'»Só3‡½RÇ7•Û»ÍVŸÏsN<×µ=œ9˜ *GC»\Ê–d¤Ü þê6C¯êÞd~±Q£håu˸¾Ô[‡cŠè˜{­ [‡ÃetðzV$°ßQûö«Í@øD²&=‹g÷ÎåââÒ±å•,Ég¹Aœ§c5,áT:z~4Ùc6^;7ñ÷4ÏÙáòÌH:a´=Ô;*pä…sÔ¸o0Ŷû» ­pD@0”¥o™tÿø@^ý\½GA3$ ôMç_¼øJ@¬Ç™\ÔÉ ¨)liÐÔÔ5ÔL~9±Þc¹F9{O7"FE¬±}F^ÃqTTNðEvã‘yi£käK­]ä:Z½UËB3³™tmkW ȃ2[²1‚•TS¦<$·yQ:B%‡!a?Þ©”a$êÕ«7ìS^ ñçFƒÜ àv88àg$5$n7+™$K½Ô¥\^yrvt`}¦ ¾î1 ¶=§ÜèkëµcªË€ÿa]DÌ|ÓT½%*¾ùV1¨Á¢šh®:ÕgÙ ÀA&‚ð¦ç+ ¼áÀsÝ—`Û5Ôfpx¬°˜|6›@³{‡œx²cia²®ó‚ö)tã”JE‹¶Ê(Ð2rSQo¦Aç¬õYܬ–r0hr ûøÃǨ`]ÓŸåYGŸÛ}³_{‰¾¤…³j½ ÐIHV‹ ù` m’ŠÂ5GI—òÿóÿ+aÄŽ´%”]Y—ªVR—[ÌhD–bœCê˜}ôwŒ‰+¥c¯¬›žyc>.3_zï2•.s­ÿƒQüôh(ÿíÍ é_C2ÚD¤p5ZÇ’-¥÷\— wèÚÚVN¸ä}.yo‹ö¨¬¼Xz¼ƒ,¯XÆÄ{8âzôȈˆÊOW 8æ(/k'YWCóTùã÷4Ð(¶ÁÊ][? ×/«½@¦.ÿêúb¤K\Öž»²:²ÔÄÛ‰ „ž¡^ñœÓ*’Eš1/\`­{‰óàõd §Ç§r;YÊW—w2^:L¤g=eõ#$9ðlìäoŸN¥©†Ï@M¶é¿óïÊíí­ÜÝÝn!kØFNºkfå³Må¤Õöh jÀYaÁâÉ·¼Âµ@;¨ z¾¥qg²Ú‡3£žæ0´r*W8x•ËN½²®ê˜CCEÀÆ ßuâõC ˆ Ë{q½’/^ªq‡d-Dô*‡íHOòæzfsáéÉY‘cY°§lEP\?Õ¬®éTùïêÎ[ÏßûˆîG‹±î3oË<‡ ¡pÙ&÷ÀD$íí}Hþn \·|ƒ~°¤x~Þ/7D¹6㵞oð't¥ÝDÐbˆçG÷õójp× ]IW£Ÿ%JÛ¡iÀI_^¤ĵR=$žäþýòóŸÿœà,Ç,—sö2=GŠrqq%ô÷bÃ9ø†aqŸQº‡xá™â¸Êcè7@`Èeð™§ç.ç„@áÚUÄdbÔ¢{=òúñÞ<ÕÌ9„ƒ¬•þ"]“‡òüùk 3é÷r¶]Vè±j†áz[LFØa [RZI–ûE#ŒUÕ‚.ÄéߦS_¢Ž¡î·ìÈÒŠïÍDC.$P«Q ,sAø*!œ_so$ºÎëèšÄ)jFë§…#=ò·’£,îªAf[Iç´3•¬ †?]ô,d §Ò¨¾Š1ýÛHžf~ÕBqœV¦'âÎë¶õ‹ÄNƒ~Mð¥C×]×Ù^[¿Õ“p¾f6n7O=x,OŸ>eưèÝÁBË_l<ë3Iä°Àì ÌÀ󶪛U=Š’ÓÕ8$ ö–”-€n€N‚鱬§¯4È="ŸI¬-=¿@ƒ}3K7²Ð$ß”dÿ”¹î#uðãΪ° ¸J$‰tïc–Ñ9—Ð*XἜˆqV6*Sô+@k±Uµlªë•Ïk™/5¹iê>SCI®Ðç|ø]ùâË7ºþg¼¯^3!Ùh…aO ô‡–O~f!a`ˆ.·YP káQøþþïÁ™Æp襻ñ>XOj+vQësoñpæNbÏ•v_hDûàø˜œ¶âÙëÔFЩSY:ôw™"úä“ÂfÕƒ?BkŠþ÷Q'Ñ >Pß î6Kz,w· ÍéAHPSÚ&d Ûñߊ챆U•SP{ï}/]ÓùÀrF,•ÕåR‹„®FSÄzÒi5etÍ }3ò¢ë?$òfº¶’Ö"—%Jð #øá0tE¹«UVsIWþ6`õ|=/äAÇsNP³pþíËÁ_|, -.¾x+“·›-b¿ÚÃ) ž™åÖ{þå7wD¡Z«su{]9>9’_üòWÆå]˜¡_-t£7fRD•£iD®³ ž“5õ¥£›¶Ð@èR³5TZÚ1zHžLSðµ×ÙjÁõ1VICÅ ÆÑ¶‘{ °”ŽêW¥Í×FÜ‘Áñk8Ó˱ .¨þM-ƒ¤°žQöå§¿ë9IX~í&¹÷»ÉPúJ÷üLW%׳BêÄÁ>­] QËqz[ ”Müðcyùò%1 æ *¸™»Š–¦pÍ«å¥þýC ê¼Ý9x§ ‡€6 ÝL¶1á÷½H¥DËd×3Ý-"sq¼ÿ6CÞìí8Ìk²ŸÛk¹üÇœÁ8²qOÛ`©©öùyÉ>ót1Òs²a6Z¬]«Âý§qƒÁŸbÐPG¬ÁIƒ1#zuÐ+Œi•[çÇŒ¶°¹qhi£L_Pî´¦e®H¹ÜÈÇ`G$€4¢¦:MÊ•Œ¬çêD5 ¯ñ)ÈëŒÜ”Ëü­ÀL½· ¦ºg}J<—a„ÆÃh—‘ÍÁ±éû±ý-ϵ"Ù³®Ëë¥Q¢¶&_NJð¤Ð&â>ÓÛ™tt#Jpâçd>™ËÝêN¼³J²a¡ÈŠ•‹2¹¾DMg¬§k³d‹j;³[¹šjìÑ-¯*"Þ =á ïÆ-©y¦¶/–Éõ3Iâûâ"kA¬¬›àJŽ`ºÔœ™¾œDŽ‚ óžU"§–‡Øa2-5ÈX]/[ íDúR÷F½ð\3ˆÎ°”në¡”ë#G«jŽÉÆ\÷ØÜ¤4¸éªmië{A¬(‘/¿ºÖs‚àd#í JjOª…Ì‹ù.¨|è£r¶ä¤ yܽZØÍóo¿ÙøFJ F¯n^ÉÅø™tºMfüÌò֥ɺ!ŽøUù‰Ë¦4yÍ¶Ë ”uáÞÜŽäì`@£›9&7”©=î²óz o‘8’8ùe¹Çë^—¦ô~1Z˜*¸Ã=|¦òSšågÈùl òµügÀA¼-É(½í(÷Ά·çÌ-]@ßÁJd&Ø‘é"¯×k=‹Æfª54}`q÷É>”ÞÌÓ×òƒ™úÕ^&¹~ðgg]¦Î«b7ÎTº9 öÊœ‘>P£õrUÉ]†;ônBc{ ÌrR˜˜ÝµFÙ'º¸Ãf(Ÿž äñg$éÁ[MnçâCé¢X“t§¨#o÷,àÀÁ†6ÚdòÕX7ªRÓžÇu ýú-É&˜ac¼O·[Þªq,%îFÄ 6Š8ì4õ5:–Å Ç0¢î—ñò…,uóG˜ÉÖ¢ÚXgF}éð)ˆjžùw‘j(‡ç®ŒQ84®(öro,e~‚¯Ôȵ-hÙÊ¢–®·Š™êF"?ûíµÌÒX#{ÛkîtØskÊåõb#,¤§ZÈŸ~2ä\ûb‘Ëë«¥ÜÜÌḛ́T)˜ÂêJìV¤f´3t“zä¡‘­ˆÈžò\ñÕ¤/øú›o_Ê'Ÿ|h1MµG÷»×#‚ú÷+yø0aµ™2´Ärê5—Fôáz¢¡ûãÒq»W4‰V²Í(RÑ”áЕ™¡¨ÆòøøD~öóŸ9e5H•ΨØFyß¾×ÄøW°ÒÀàNŽ4 *÷f0=‡'Åü D{§pC4ž“,-­?ë‹ÓG&U±ÀàÉ=û»[M"CÅ“”ª°^ç?ü?cÍ®º&YªN#iu¥ß?”ç/~/XŠ1Õk^hFGF·ÂFk±~è_£B0/ˆˆïôEÆs8ÏFý ÇèœHV²‘fŒ¨DE²wÊÁQGÎ{ýA65Çè·-[ç Í4 s@îöÀ‚°1YXp$q6F0´tHzÃî@À£fDó¬ìH–­…XϪžùÕ5¿|§€º›Õkœ€†ªeÞ znÕ4P¡†–ÝϤ4dФ}Ú”•ãóµ> } àìhè ôþÀ«~òÁ‰üöí?J&ºl-wï¡Ò€½ÊFƒ_½x.Ož|¢ß_™ž»cójõO1ðP¬éc”nKxVm¨*Ü^üN÷AÉ1J$ ­$áß!È&…¶ÚïëË™ÚëG7As]ú £?TÁê@N¾û Ð=|=ºæNAÒW“ÙÚuoÇý‚x•ÈÓ¢ÛÒ³nj“x?Øýù"$…u­ ºÞ$¨ â‚N€h³`€~:ìN«¥62jHª†,ÎÜø‘+3úN-m1]û›É§.F/¥ÓkI„À<†D#³Å@–ì›>.êýP:Û ¨Tìg,“ÚßàØ£Ýüv:%ºw ´¡Æß‘Æ~i8´ú²¦õ¬Œ Ä4Þi¹ë¡}9ÑÜL¶¥‚ÚR”¹I²ôçÄVÒ33´sØ{Âò®(‡·'Y[—ºÜgÆ]a·L´#™kýf”9ÀTeúÐbótßY¥òêâZžŸÚ8‡[†zòï´ .ûúfmäDç[P´N0¤<˜Ø»]½'€ºnAžugˆ}Î,7c³‘åM#ßê´>>Pc«)õòZäÆg=¥‘äx±¢„¥îJÎŒJGóù³[  cäÚ«q9=;£¬aMúAä>æó×S9hîh(÷OÛ Yæ>ËÞ4üe¹+{‚èB ëéù}Y?}Î5Ƈ¦®ý:³,±t­"¿+ÛÁàDD XˆVȱ7ö×CçÈ÷2nÌå/˜j¶ÀX‹ÆÖ-£Ùè~EÛa ÿ¾¼[É·W@OÇ 8é2Lbééý‚ Ò»ÍVÅçLùýWmÍ8|b݆ŸôåÅ«‘üá‹7V} …oæÒzô•#ŒÅ>5¤M½ |‹µ¿m'8%Q£þÌkΈ®÷7ß>•'qÕ¿¿íµÆÎ¹j5ü³Y.½~B*Ýv²âäšÎFnæŸ8ìCvn¶ <›M÷œÒq]iòBžáÓgÏÔy¯¹–Ø«sÍlKGmZK±j¢&ÍйWr|OƒŠfS*ð P <¤¾séïŽr¶ÐgàŒgï›±@JkB)Ï$®Ù bR5j—…$šEAYmµîÊ›ßk6X4ôµcǦÊÉñ©—r~f­pÈç7,gÇm 8ÚÒd8Ìî P§ZQü¥ªv\Ä(N¤™œn5°Ö«d•&•Ï€r³2ŽúdeÝ”f¡g „Gžñ¶£ï `ºÑ{Ö3íœÑ團 ¿cq¡kå„n’'ôwb3¬ŽWÆí¾_®AmÀÇóŒþq•¾•Öù s™¹vÎfÀkÛ'‡ƒ½õÖ’tbé¯zê´ ð¬ʇe¤ÉÄ™'ôPÞf¯%of ªæy( H£j$í¥&|Eov6M&ž}ûµfð?2éÑÒ*BÞê³²Îõ]/3ªObê`:z¦É4PRG_\ ÁeÀ€n³–›‹¤‹–±Ò¤9ÄJÏ´Ã5(óÔi“œÊeùÆ'I …Ë××2&9C`…™r3ñ’3àðuaÙäÄBEŒHÎÀt¹^Êralˆx]T¾@U]Ë"h#;å²ZÚÝXNÎÕ&¾6‰ƒùꟾ’ìz%·/.m´Ï3¿â¹¦¹?(ˆ…€®Z2~t£{@ƒÎNdUøŒµF oÒ…ÆÃìÖ  ÔÙæÈÑ#n:‡雽R£‡H¶Å¦½C {;q  ØO|ë oÊÝl9ƒ×/Ö¼Q¾ÖC6WcƒÍU³«…D8y\Øzþ¶v(È`}o'ƒèrq‡(ÞŒ*©ÿíæ|ÙÈY™j‹ö7UΞËqOpëT.' –ÙmÞÕ$¹ÈŽÝËz¢¼¾hpÓ£˜Œõ‹ i«#ŸOÆr§Ùâï/&ݦN¨ÄãýÒÉ¢<ͬÄ)†a)ou×Ñ(P3Û˜Þ0*8ÔÍPW#0r“åYoJRK³aâ±/ÖL_pž^¹×µ@ _¿©Á÷tCÅm€ Èw5ÿæ›oLw†šô´©´Ë©Ý©#ÔàEß{‰RXùÏgí¬ÒRJ'iÊ'¹¿~saÑ®%v" ù¶_ˆV·òzájÈž#Û(õ °Åõœt\@¯ä}ÜE(W£µîGŸå{œTuŸS³®U‘’üçïþ WK×4’~âËq7¤ô)fŠQ:†QÄþZéý~òP~ç´â"79žy&ggC¹ºšÈíhÁ¨ÝH£Uà*vÎÆÇ8.kF” .÷åÆª]0b´©öáˆÕ§òâå+Êî+é•ÛJ}oªÙß_ÿ§…´;Crµw!g‰ÐI3c¾•Óó6§™;úÜ]Ù_œSßv‹‚][ Ïx<áóбò×ìÙ•.èªï+Žv…”7oÞÈ“<ѵH‚Âu¹Ñ3u™q¾xv—›V(EqYêfShˆû `‘+€ÕŠSê`ã¹®©:N”'o¿É¤Ón3‡=8ÈÉI(ÓÉCÖç2žŽx¦Ñ.zðÑ}iè:èKIAÇÒiòÀ?—W¯^Êæ®"ÙKÕ×ýyg€±Òͺû¹³“ 9Zú÷Ôyû=+½æ™,§ñ ŽÌlÔJ÷Á,Ñ{q­ ð²E Žñ‚%hò aº‰Î?}¨AÀJ÷á’ývd}›¥aš|'œTĵ’[¬.°ô­ï™ ƒrŽrzåTÆLr¡™þòæB†¤¯– ‚P€ Lp%D`0RX®eå-¤±è©­ö9£Ý ñ¿ÇvŽ×ñ$s¹}{Ëõc"Òü@†êüîæ¯ ãR3ÆUF+ °íó¯ôyæ–…oK¶V) RžYœI©ûc6(f“j&ä_I§5`Å í¿¶‰kMR2ˆ i–;¹[ÊôN'ç¼sîÃn%ÍüHîPÖõ†½¿ÕýÀ”DEFBχøK"··s‚4Á3G¹~Ü©-lщ\ˆ‰Ìš½ZÌe¤ÁÜr³¶sTeL8ò=Tþî#2Ø…¡µâ¦š‰—a*¯/^Õ8 &¾“…nbÔ®!M(ñ¡¢­Y¿Û“°§Q!JUp,¨¸qæ´®×9êRÏÕÂ*§½Š1 ­ }8à®.2ê—·òñƒSý~È·UD¹NëÉã²|µ„v¢†û6‚|Ò¤Ž"à¿fŽ¥ÝŒJ[F²tÚΉû{À¢morÛ£ô÷²qoK½({Ž<×x¼®DŠLä"ÈTpo>8“ß={m²ƒUåPÑ•‘ ˆ3Ü ÑkýæÅ…tˆ4ªt}ªó†µ&›‚AMåÐJ˜'F™Ù↯¤¥1®oθ¥³£]'(ÉÈÖW'ŸübYßS5(c °d»ùÖc±~;G¾ qºOùš+BÆ|Ö©äzn‹j%¿Æ&F†PŒË*óˆäöÞüÉËÝ×™:j”ëñ=dÏ8”Û19ñ›Ý¸Êâ¼z6é~»'GnÙNåY/,ûÝnÑliˆ2›çݳ>cµ°Y}?]ÿàûÑ"õòNÇË“J¿‘ûYZxÙ&6ÿêøÇÅ‹WãD÷Sª™Ræ‡áF×0Ûdš©uÐh< Â0ןmBü,r5Âëªòòõj²&¤ VÄã¢æúý 2æý®Ÿ7ã*o4ƒ¾Y¡{»LazxÐY§ºHˆ¿¸ðU«j©àË‹¼úá_ö “›õªŒ!¡·ò£-kW³ù\Ëõ&ˆc5KQè«ƒÕøM¯Zÿ/ËRZÜ •DúVø@÷tC¯!ÔÏ¡°Æð¸,Ç£ec:Þ´Ô6Äz6#Ž47ªPqSß+Ä%V¥þY ž'úƨ¬Ô/”‘„E#>Ðç‡y¼Y±žÛH÷Ÿæìø‡ƒ£ VÓBƬè¥eµj†aÆ t2½Ž*Ô$„©WÏn“Óã}ÿH—ß÷ÏÏBïOþä¡wqñÒ+ë=ªYÓzÑ•Í7לÆ,FŒÚê«:‘ž÷{ò|zI¬p0Ý>(Y­´íny)²ñyJí /+Iì“k Á‹xÜ–ªë˵wkzºoÃ;u~ìÆá|ã–ÏQ1KYöÞ,Ôø«è|Ô“5ÐùaW:š °â¡×øô¾±^}^nUѶ" ¢ž‰e„©gªÒ÷+ÇÖ‚S`YO¯’Þ§PÊ­YÁ¸£Á‘4“&(œ<9-€çO4˜U;’ƒ d ¯£Y&uC"{oÌúxüìÛgÄÈP4)oH[¯ï^ÿT}ïëñ•#G² °ämÂÍŠ`·ºGä9|Tͽ`hm½îj©óH"p‘/¿%J>Àγ¾8Þ3U?PèçÙLø¾Iý™fª)+†°·ú;µ'<ô@ßkc¸°À#1KQDD{Á’jeœL¸Ô¾Á¶¸4#±ˆ[€F…[ûãÇߓח7r3ºdù~“/9 œJø ½f¿¥‰Ùa[¾ûý¥¶`Hg°–¿ÿÃ?Êó‹§`&Ò)cµ³+*ïÕÕ)T“(yl²îêø7 ?ôåV#²kÔø “f#¢:ihÔjÒ©X \P»QàãÆå³ËcdJÄ«·×òøþKçµD¤ç2øÔ‰TàgÈÛ×åÎÈ#8øb¶‘sÝLjådá›ãd6ÇŒ S§Ê@ÁgêØÇó…Fž™ã÷¶¥tÏiûN]Ç«{æ5@ijß×W7.TsO›pBà$PM«šÌf}M¦²\ÌÈŒ[wTžQ“r@cºãQ¹I7Û * ¬|ºvý]Cú»Þ­+wo 7'›èç3=@ ½÷35êo¤b¡ à QÔóÔžÌt¬Xöñåmî‘Â=< kP÷JM¥ªIÖ‹ÆÌ£`”}¸54{üðãäóÏÿÁÐän݃b%-™Y« ™·Pal\Dlí„5ž=*=O ë?|©{Oƒ0 ’¢F«:ì6ªs¯QNÕ6’V‘´ZêÀ’,ÖÍ¢Ó0Š7Õ®[ ™7êeæúü–úlçGe°È ¦Ïz¡×>Ïòrqúƒ æ]#îôïúh¤Ï)ƒ@lË[hd7%A•§ÓÍ,$°0—C¸S›c®£¯aÑýçÿó¿+Wÿé'>(…ëÊxyµ .ëÀßVЏ«bùž•kwˆ x7.ûÚXÿ€(­Ó‘3˜ž˜í¦ÆsØš”öõ<£û,+ƒ±V.}Ç>›Ï–ŒÐŽ{àÚ±¼¥{ QDÙ®E•;ŽO¯–ªÐ}ôÙg†ã(ÿáÿû‰&WÞVRÖ¡dMu7°R1ª!õtKYº^1hKÉJT’èÆtÉÃí¸i³AŸ\žd›D1¨yÓ¿ø‹Sþç¿ùÿ;|ª¯'ÍŽõ5z ¦»ýúÂÅvÁäazÍÀfÉá\F-µ«- æ i‰¬naµ´ªFºRG¤N¡wÖ´õ,$½‰e}—‘í1l—réªQH6È:Hz;Ùüú­>›$;unc ®ÕÁ©ëÚ— {>^f“%FÂq%‰žû¦¦ó¼Ò8dß%ê©7‘B3mLZÐJÈ–Ö²ÅþMëÉ 7áÛ8**‚Ýä„ýkd݇ lV ™.¬É‘ª|ÑþùÖ¢ý-wƶ7FßÐ÷ÒëÎô56«+éµá{ÌÎÁ¼N‘ÕbA»®‡Øè÷Gúï+ 9Ø¢ L˜‰Í È`F¾¯IK¹q‰áÅpý»¾có[?cÄ>|`vv5‹¨Fm znG·ÌÒŸ<ú—òâõknlåà&X6àîñÁ™œÉî9Ie»oØÜŸüô'r;»”Á «ûÃ*±dw›ÜÆ/Äuiúê@–ˆh¿\éëüßþiu­‡ø ½W4ÿÙ‡0„8¢#\è×Ïž;ÞÏXŽ<[ŠÃµõ!NM–·ÛlÈã{§NLb7K ðÛ=ßÊéž+‰n*côúFR}8œÇA.ôÎg¦ãH‹":)Lž¥NdçKµ`­ÅT¦®†‡ŸÛ„"íÈš½x•ƒºG¾ïäIhPÉN—»Ü6&zP¾¾Ý Âc¢™M.´”¯Ÿ~kB ŽHÖ8ðw³Â¾Ç,U2Åék5ìþ!oºÈw­jHbãÔçlˆ¦‘¶^Û³¥Í=¶4Øi6›,o¡´Är±#7€õ¯¿çIÝòþ¹Z‹ûþV©®Ì·Üä2ê}|ç;ß#Ê/õ+–ààæº©¢t"I^÷㞬¢vg6£,÷£µFŸ+œ?ˆôCCâ œéçYåã€þTž;]×kL½½¼™Íæ[ ÊVeM4›àØø;´®ã½÷œp€roÆÒY}g(w§n=ÿµ:ÕŸþ—¿ñ3軽‡ýÚA_Ýc÷Z®æû½M=4íØÈ\Pîª85°Ùβ£s,åx_½G~@g›Úû/S3!ÏAaÜçY^ó¯{[~roO†D8éxû`x/©)¼ùNo½•ÔÊx»GG’ÎÏ.èÇÿâß•ýþ«¿ÎÚ®Tg{0ô7Vڥȴ Y903ÉÎZ#~ÛnÚ““Ù’ÚˆìU®¯Ùœsž¶p`1ôq\€v­+fA 2](‘6Ý)„ÕÛs½8тϑ«mæG•ÐjsûØ‚ßÂ9P‚WûÕJˆùluáY‚2­€²0¼&QÀ˜£Ïûµjí¡”ì;_s»\h·ºªí‚ó"˜Ñbob —ùÜ2TuõÝ~¯©Ï÷¸(Ê“*¨Ê 8žÜŽä×ÅADC Ê ÐÕ7éêZ·7ëU¢‰R²Þ¬óù,žysÜ×WîûÁ*ιÜWO7²g,¯g¯æ.˜ñ¨®†Ù~~Öó¾ðS™s9VÛ êÖhÕ”Ë t²šL¤jÚ(ËTúZ]u„p†q#ÞN‰ƒ"ÛœÄwš<Ì¥€tø²Ë6@ €EpÉÏnnŸÁ«ž•er(]i§£:ŸçàÕˬÂ1xX6jlž=SÈ7#˜ ]6fÁ5 F"™¯SÉ߬%ŸåT%‹‰‡ú;j[úóÅSR¶\£¨/ú£ʷϾ–/þé·R©½.Õ‘ãçØˆÕª«¾Õl¸(©ë–0_éë, pö½¦úh®O¤?À{´ØŠ ËŸ—é_¤1»hu€t 9èVŇ&Ààÿg콂,»Îs±µÖ'tš€9¢ ‘b&0¡Ó Á QÒ½÷å¾úÅ~t•Ëe¿ºlW¹Êï~ó‹oݲoÙ¢ "Îtš @ˆ”HQ$D“z:œ´÷^ËZaŸî¡©9==§ÏÙaíõ§/ÀÚ©®9Šsá|iÚ¨ÎwîÌ7Õ±£GƒB»#Vêåõ—HÜÏY.šK`ŒÉM.Ø ö{oê\MF8‡‡Ý·©#Ç߃ãê¨üØÉûÔáá[±5ŸÞ4Uæ(rß½ê½> tãçÂo›À]ëSÕxoÔ6ÿ¯]S÷ž¸«ÅÇÀzCó¬¼#<^œ»üf¿R× ˆÏæ¹ ü £Æ.ö.ÞuÑ¥fç1žõâEœ…‹w¢ã½PâìxíD3ÐhanFýÑÃ÷©__ÛV×v9‹Ù‡Šjí&Éö {˜:Ù°f$ó>1«Ï˾€Þx@éÛ´øâqÍ”…»ï¹[ýò½T‡ä € ¬ªÄ(DÐÒ™lþ•kkŸ{’i‘¯¡qñÐyöJšæèX„ö²¢Eo¼æG^³N½ •Ø/J8Ú¢lʼ„ø[TÔÆð÷¡†à Uì^oföö‰Ç>³ý»wß»yÏW—>…Åw ‚ÛG°!~Ëï=¨H>ÂJŸesšX$äÚ¬“y/º‘‘›ÈʹîÍ­uƒÎu^f4YÎFZÀ>Óñ€¼LPÿ•ËÔg­O‚.o®Üìñ>mmnóÔ3©÷Qç=Cjïâ\‰sð¢ÄGRíó#$\s¬’ÙÅn´ÒAÛXü E©ŒÌi:†Qæ“ùÑâ“,]Ÿ¤¥šŒE‘@éld6äÈÏ7žj>Ã\¿>Æè&gÑúò«›Ä3ÿBqYlJ,*9ó|Ï*¿ô/ãY:VZ^ÀFIr`$Øc…î WNY±[ë†gzɹŠP?öz';\Êà6:>ÅtQîhÔt>$EK8 'ŸÏñÈˆÚ þM^°cjƨ ¨pûÛa|k=¡€O×<^î$%Ç5˘,#ÉŸ¿ÑÔYÉMÃŽÁœ|ËXŒÊ• SKo7;#È Þs•~O ­«Ó̩٦únL&ì–¦gƒTï †ãÍjÕ›'Ê0©©-Ì9aïW÷m2xp¸=¼¿ºt²²ÕÝM]ƒ;yÞaÁá‘k;ss÷V¯êÚüØ='p,oÛÛe'/²ÞÜ üêë}LUs64jþø‚êw»4ŸÇbŒêMç½ÑS3QûÙujtÜeã,pù9õêÅ—Õ$8›ŸïžÆ¼k…2ºcEÞá(ûŠ4aL:P‹}oG,Œ‡Èàç<’9ÚÓPdéÞ{Ó“PL®ßøD­_¹HA߃“_ÛŠºyx~˜áüÜAP×Í9,¾ ˆÝ³d˛߸½£¶1â‹T”9Hó„ 8GUðG×>eàR#Â,&¶s¹¥ìhV„-Øw¯Ý„ «TwY`Óu–A7(¤ À"æ÷aÿî÷ÑNÉmG©€±…Ý”­`1Ü‚“#Š\ÕPdàkžÐrj½cÅR#ÂPžÌ»û]õØ]GգǨOá¢ßŒ©ºÂA{È5„cýí È:Ç "A†2S"l½•ò†œ¬0<äèy‹§QGU' yøøÖ.©¡a€ÎÐï¢XIÝæTÂþ~Ïϵˆ;u$SI@j ng êøÝó”5` ÎQ¯EoÆbºì÷'eÙÀÏp=öó¼ÜΊ¾ÊY^| ×óSxèÞ5yñ\çßN{]'¼ù€¨J8ö¢%$"hDe çd†Dša-¯÷›võÑ´1{¯k%Èx“H£âK¯Bð]\YµkkfÜp˜ÌŒ×%w-Ò„Ž4^“‰ÍÕúú:í ¼ºJ»KéD ƧK5UâGûì•Î@¯ŒPú¤àç9öT‘C5Þ…‡¶"©w¦úHÀÊF^‹@ Îàg{¨Õ®‚Тð'µ`K$z;éZùëBIØéÀ~tÌÙÎâ'˜¤ te‘«ðHoº1Òd¨2u¥7‰<“¦¶z† ;h‘´¬®QÞä˜0ª-ÎmNG6¦,C™ººñÆ­EB¯®#Àîò¥5³÷òÍý‹\©‘ -”9¬ÄëÆû(ÆÄ%@¡ÊEÍu[{Àsskø9Ý{q[¤‘OW·Ž3tÕD™‘PÕ°§áý´‚ÒŸ4cšõ*íyÓ‰IäS«Ñ ]¾Ö—œ£“L&âbØm±‚è¥Ü×ð¾ÈEç¬V†|\r ”{CèœgKLJ+,B Š«àÃús†I#ÐíÛÛ×\f®™žú‡înI¨Oácg¶HE»ï蘮Ñùo?mqFnážþÓÏq|w8|Dú¾ý½Ý‡w®ßz¨Óä÷Ú®:õÝx¦@58SÛI4—Ãɠܯ÷²muÃÌöçU—ôÔ{Ôé¼öé'±Ãd.ó3zd…”ê±/}^?z¡©ùÖ¯Ô›ŸR×ÄBv{æ«_T×:¢®l­Óü¾À YÞÍ”CXHû¸³Qf™ž­ªb—¯Ý ™‡ ˆKË>R¡è îá8†¤þÌ©EG̪ٻ¨{vQïþî·”õt]Ó>áYLÔ%ƒ¯ûù#u’Ÿ§Î=­^Y{‚wF \¶1’]ê°ÇÂ’b‰feÉW£ðŒ#Ð'–¬øˆÏŸS_ùâ¹3«4ÿ_þúŸÕÏýSz*ÈÆÕ0&€‚xM ½ÔÛÿÜgÿ@½ñã7ág JcYŒÛTœº÷îc*¿ »Ôˆø¿¢t¦Zûy¨ï>znÈX]¿};ÒÑhSŠoüàà/½ûÉ5jùÌõû±M7fß5µÑq{LÁ/ù[¦t¡b›¦4$kÃü÷ábÔ£:áx¦GónZó–ZLÕ„R}H>®C%®á3ЀÅë¸ãñõËÞ?ú–ïŽwx‚HRªrEzÍ qˆfü(\ï+ÔLfB>p/\“ýÐÁ ç7 úH“š@65±mšºyÏð´áboƽ٦ìõ«™NwÔï÷³½r§ž=º½0wüz·×û¸,;ò]còßÂý$`×E •7ᓵJâjð÷ÆÜÚo§ÚMtЩ÷SЦ×0hØNÓorhIJ¯ AÅ®.é!1,ÚBŠc†g¸:j?Ð1]ÚX3«««öÕ‹7E4„¡–09㦵)ºÏ¾FZÊ9ÐúKPoâÛBYmãõÀuP‹ ÌÄÁŠ9¶Ù‚‰—àèd¶©3‹+víâš™-RD¼öª^˜ÅÛÆb+¼q*tot¢bh=H@ Þ§‚ìuÂ"C)¾BX™ãó€T´Æ;ëàúBa¡JSv?sµç+q¡]È3™‘áA *~†Ê’ùþìà c&•&+䥆8ñŽãñ$¾»„¿túÌ’½ry-X&¼þúšA{vÔf]rû‚÷VVð j‚®5à’ºL´~ºFì]‡Œ)éëÐ5 ,Ãf5ÖúJ?zrê$/ ¯—@n˜Ày½ w¬Q‚Åa5ÐjiiÅn]Y#Á0-£Qæ„Æ']Ú…n í=jäŒk)¨`$ƒœ«_t¬ÂQHmmDjW‚8V¿plv§)y¤7:7Z­œ9o ¨Ü/|Ã`×Ö¢¥­#]+¸`½×h^S±ûù/üñ W›°†Þܼ´aNvO¹ñ”¦……H;68v±³Íñ™c³«ñÃ¥š9…ËÉ_ýÓ/¾<¬†wëÑ‘Q5šN†ýÊMºvŠ¢ß5Gçï6D3…›õ¹Gþ@}AêÃÞR‰-ü{>ó%õÁ»ï©Û×±ÈÃîÑ ²”/A€¼µ½ÍŽ»™C48B©?.U×¶éÃÚÄ  @wn^]¯êÏ<Å^M{Ïc~V½ûÖ¿À¥4Ô!E4½£ ×c½ƒdtäàB}xc[ÍÀC9ßﻂªc%Aœ˜øÕaæ€È3­Í5â×ßÙ«ãð¯S1ƒIVÜ]ßž‡·»|uC}rór ÌrÙ‹„®K~Õ˜Ø }3¯Î?½¢.¬­S¢]OŒZð¿OÉ·Î?«òΜ]R‰ë(£’ÜÑ9qñÄoí¸ˆ­dÜà Ó 2:ç: êPì?U?x?+Ca6ÆP•ýŽº UíÝ'N¨¹«<À{ÈOÆœ£fÓ±²ñœƒ÷ .D­úÍ “–RÕ\§¢–=н}G&FO'`2‘p–e vdTk0úD%B.:–I…åO ÝÂX]“-‰QLj'&²™Ie‚4;ì0ÙOR‰³G<·B¨…ÕèlL¾…ÃQ!²®:Üj¯ôG÷±‘ŠÜÅuÃøÝJŠ®`ðK„^*˾ÕpýZ›L0ÑÐŒkÊk²q䨈"+ì±L«w B“Èú -úë.>^Ü´â“ Ö¹žÒ†ð]Ò®f°.^4t¨Âî@µ§åý„æìïArøæxsšxóµ8Æ¡|/Ÿ¡@A`@\–#Þ”±Ë“‰͌ރ„“DŠ“–Z±´4ôìXøŒåÓçl6›Ø ç’”$tä¸ÂC¨ãzÏßõ—.˜Õ??g•¿W8:Âãññ¨Ìĵ"ÏŒ³.$³cܪoLÆ7o›ÿd¼ä=»"¬ ·‹u~éYä“dëSSmõôÏB,ÛùcV/›„1Êsl-Uß6è™Gyç äAbÕ©™ÒçÛh'W·E”¦mi¤tP£K'Pr'Y7ŠlÀ«{9 ÞK „ÊÈZ|†T!`@In¤*'ßtåȳ|£'j¦t™œl(ñ±ÐÉX„…W¸9ÇòÒË[&VŸÉN”䘆ÚJúdÔ0óuM" ¡åµ„­~¬N¬ëªc¦óÀ”î6u]âTÒåIÁw±µžÎðùe:ÎJ=ýÔ’}ù…ËÆI&…œïz¤YŽ~v _…#B׋ΒæG®5c‡.¤B9ÁH o»s.`SœŠÏ%á2º!·3ì< ‹¤`$0ÒШ•{×òò*ñB¹’çQt¬µ´Ô±ý?ÒIbỿ?¾~ä=’¹kðÚ…MC`A‘u¥ÄƧH¢^‰dL *¤™I~\³ðL#|o_Ùg?)¦,¨÷õë·! {{ù™oZŠ ÔÊæYÓk.›ÏÍýz|áqQ»4Áªü¸š|nROxrÿ`4¸gwïøµý›sÛãݾ™ŒÊñpœí«1áκw°ŠÍÑcGÅã‡ï½£v¶o«Çÿ£Ð¡@  n+({aßËd­`™ µõø±­ÎE’&™WŠ=°'a%¾ 1i4ÒºCu·Ç~T=öÀçÔ¥7^U·v'ÄpÒ4xÅäæ|ÍÀû|Ç€&BµúÙ¯Þ€g}É;"å̳›FÆ+ø:Ô‡ÿü~^ýñgþX½ûÞ{ê—¿ú9­ ªïç›Ôÿ’ºÿ“jëÇ›*¯ã B |oØøî‡*úÃ>¤PÒf¤ö³óB¾Š5·Ý]µs«£î»ÿ^u*åT>ƒ*óKÜ«ºÇ)(Ø~8RwÝÞS¿øð#u gèðº¡â*Qên4PÇ ›A zŸ#RÞÊ…¯-ó0ÃéÁg ~B³ Ë€¡v\Él·Sz,m¨ù#vfaaÒ›öæfwÊÞì²×ýô»2ÿõ‰ÙÞOMn~>عµ­ul™;)ô5 lÝzIù Û¼Ú›A9õ$<¸~Þ¼U RÅöt’*}—Z/§ë’–šTÍQ N'­GéwI oòd®¢“Ÿ³†zÓ’õ3bç=À¥]ªô/l¼—s$ûF‡í‘W•þÝ'æ ™è©_}‘º y“lê[ÑPGì !ÂëÉqõѵëÌIgN)Í9h_ª4ï¦víÖmõà¼:Ñ瀞°I|öØ1â÷߇fÈüI{û¤?V'nm«O‹ªvökÄvKAü.ÈHº$KÊ­þcX­ÀUÙµ¢Œ†ÇÕnoFÝDN¥ãd¢4"uÙŸ›¸nõú;y¯3ïö>1eçý³ôµ[EÙùe§×û)ìÌÿhRzI|áXnCR‚*¹‰#ˆBqðùj'².˜8øµßÕ,Špz²møùÏ.­pìÕ5ãÁmøµ ÙUZ™hahýæÀj`Nx e¦l«§bÅ’~H÷¸:¤ŠOpN¹'Ü!úu8Z5U­†Ç *866Ý“jÐë À¿¯_\3Kð~¯\Ü4I¾ AF$G«ƒ­É\t»3Q"+3OÓÒ¬‡é÷ë,J¿ÑH$çVnE%öt¦$ÅÜÔ4“ŽÅçÚƒô´⊓ÚöE¬Ð¬ˆ*žñâ$;˜øJÖ%]\¥¹ôÝ‹2#¦å W6B£;"¡›&kFhsN*XŸ¤°§5©'‹“\‘ñû4Öô<'O8.+€^L6"QEÓ}}úÙU²M¶µõDüβkTŸÂà‰Vš,š/æ,Þ‘Ì$u’›òÜÑQ°sÔR%·.šejB†{D½¯wcPO‘õ†Ä<eÎAÚ—a5S®ÕÊâ9‹‹.cuŽÏïþ—lçÊ–|S£¡ØÎö·Ñ-LK¬J £ìXHˆyKBO¦S©¢ÛÐýnBÛGFXÁþµþƒKfù»KÖõ*:πТSOk¨Žèyªs‘n ßÖÚ¦Y‚ûsåÂ%®Ä?(Z3~R;eM+ âJ%¦)ðgɆ=£¢›«%arÓ®SpíFcuéÅ—L§`ÿ‚ Áꢯ9&ZNp^!­Ñ.ö›X‘àç+ý¢_],.Ù\´·ÎÌ«ºúre›Ç'Uõ(ì…Nlsb4-ìFýá`P–÷j³¿ƒzè{œÀjn¡#»>±+ƒß#Rå$ƒ³ê\÷ˆ¿|ýeC{_3—sÛ¹ …›WÜÊúªÉJ!/ l\Òçÿ·A܃ÈÍ„ìýHÄ2ô†¡á*hl}p‰ã53…RoWäZæ{²ÑcÝ>ÄdXˆËK‹öËkÆÌœä|ïçJ±T£‰èr„g›8ŠAÄ­q¯wijפQœ‰³ß—\}f»ìRftº¹$]—tœÐêŒ8Ò ¹YA6³ô,2ãG¸BK»ñA5ãÙµRq&ÞŸaŸ`2¿;Ì €ñÚaë[ä˜38è:¢˜§ƒ¶¹!i¥ “VÍlñ© çÔXœÑö‡ø»’,Æ÷Òašî¢¿5¤„Á­(N­ÍÜëZÖhÃÁ(ºv¥hu+0’Øž{ú´íÏŽ…Ù9ícä…gjcó’¡9£ãv2ÑÂ:ݣЮĹÍëØŒ4Z}úŒ½ðò&OÙP»6q#oj ,¼á‘‰€ k´éš8QДhh¥5’uzêÏn *¸ [ëüª®Ó˜€„'-…I%|{Wî–LG3I|(îeÆ-ujLN ì§N©:Ðd £&dãdØÒ*ùsë¡ mm¾™²#Ôû`äzÀ‰8¡Ó#c _ÑÉY©ÄŸXãš². fTQ“p¡ÈWò¹µ…?¼C”!1™©ÅF3´–y ¨bã1Þ†L{]y¼ ´\×Ü{ŽË§l£DÉ7²ªÎZ±åZѵ¼%ž™CV3AÙÌŠŒo^ƒ¾ÈÈÒø­ÑAã¾!}}ð»ëçGYJc ¥{jB&žêNmm¬™N1.ƒ'Wí!¸%ÐF¯îß’×ujÜbÔP¡5lLº%o¼¶¡à†ms,¤s©º3Íkºúñ]ªIw åTyÍ "dEˆ²¿¨žFØÙ&¬eObP‰I ÒÀÈ]LZÀ(~øS«vssÓ¤¬Ž4ð«ÍHbCˆ÷BÏ•t@ð¯‹K«Y_yåªÁYžðY¤™¹ SžQÈèc­mkÂ8:¨á8÷ÖÛ?íb€¾ëØq5wô~eú’v?Vçhز}ó&cV°@+2blFî@¹ŠfÛÔ¬XV|oo—~¹õøù¾ÿžúP½75ǛأJ»GNtÔñã ¼–!IC¡\Ïù´.6‰Àhn‡bŸ8ÖIwIÕn’`ôÀ}÷«·ÈPÃôûNÚO5ZA?IÁi‹ùöp¨Ž¢që­m@ÂM…ï!cªßÍ{ƒ½{îÛ.ÞýÑ]}ûÞ¢ø§¢ß¿Zk³æ…UrO·6L\¨!ºÁ&¶¸¼l/¯¯ï‘Ä_tl]5J¹qÉÿ©g„݈œú˜¢l"+ ŠTHˆ(ñZ³qÅ"ÞÚ´r¬MžIçbûòN¨ ¿ù ~ƒÏ‚¾¥ŸJõ)eÑ­ûH˜øÙ-xÈg`S=»²B×Â_3L^Váxþ–±IÒe¤ÊKèÐZŸ>fS©ø |ÿÚ%¦$!8Áqû‡„5AÂRV‚¸´ç·6a£6h^’S‚€ÉR2bç‘®0'2èž]í¦1zúðï§þcãKzÅZÐDÎeÌñÏ..[LVŒŽÊjF¼å1˜å««vÌÂQê ž·3]È–ç”7I@•V%ç¿pwͼ:³tÖ^Þzuv8ëZ:½jÙÚ©—_z F‡Áa&òÅ£5«#@œïc;ƒT.\q+’±ä¼&¥‚Û´ —¬èÛ§ÉZjÊ¢]B—SŒ˜º*ÏŸp;)ƒuÅšy.Êa Ï=´¨­ ÷f€\,?½d=:;‡szö;Ëö…¿Ù$ B{îx²éä}±I–f£ØY1=¢1,´]5¨öe ºSz–h^£–ÿD“„-©ô™3Œ›8ù† â&  ¥éÙgŸ´¯^¾H[èSßZ²˜D¼úü¦i 0&–»ä28©P%Þc‚œöó}{‚×ÀÍÚúý(ÅA}òì“¶An}Ãﻼ ÉÅÚ¦A0Ι¢U xq´ÁŽRƒEÿN`JÇìta°cW·„Sém_EÏ׿é•%º‘—×7L‘øö~£Âq…‘\ÇÅÕs+w\AGÍ™~_é’g\¸Æ 9”Õ¬Q@™óÅ¢„Ÿ=ÿcYÿˆ çñcGÔÌL‚-R‹'ê‘# ê¿ùü½ZÕîÉq]|~8qîïîÝßÙÝ+û·nç9ªDÞþDœ8áºìö˜_Ž B¦×>K}£üíÎUãÔ=±9íï"‡”dLÒyUs°4j¶W¨“w}ÖÑm.S÷…)a?¬µŽÊÏûÄ3A «ä5yí!.ŠùÍÛtÁÒ–`Šz7÷Ü£æUGçfÕ‰“'›“÷Ü3€¯[½£Ç>lúý±Zÿ ®ýÖõÝýo†j*ñ}²ÇfRÝøõŽûS' æž#>´<Ç ¶@e(Á_¹‰t²*á7á |j3bz]vm"/ÚW¤˜¬VCÕ‡ì AhCPÌo*æñjdá¼iG؆­“Ð&‹nUV÷hóQ"ƒ;ŸóòDfBÃB ŠRx‹(rÆ%y‚N¯yí¼QŠ¢`N"-¶ ¼ºYy_láä Ú ”(ÂÙºj-~|Ÿ ÜÓç—¨µn@—Òm€¤¯Js¹Y£º-†SÁµ¯M¤9i¼ay΄ªKºµ sãq©´hØÀ¤ × ÅNÝé{)7¤¤D·ºtTŒzæÜYKÀ¹\”ÃäPP™¨ZuLd0)»xÅm²@³äŸÎÁ[îx®Qk=šüô{eL0ežÌ.<–ºø÷q1 ld£—i·¹8RX&ì¥Ì€2Þ±œÉ ÞWfésU5< °„. ­Ø®6KþÌ3OXr–’ÓðÝMÅ×ùäTÄ/\5e‘µæù:U»ÄqétFê™ožµ•ñìßJæ÷ñï¶Ìd šá)ÀNûcÔaà-Xã9Æ,Å‹þð|’”ï%G2%iÕÁ ŽÈáQfpOö÷ ûTr< Ð. éUDý›~­Vþì´Åã¯høá•ñ0˜o4;ñ÷Ñðh¼€íq3b¯ct˜ “m2.ÌI¡|kœ€y¡½&kßçÔâ¢EŸòø@‹ÙÜ·+ol Q`E[LìŠ4¼7båÝí÷B‹>¸OjaÕë˜áy _#Ö¾µ¼Þ'‡T{¼ †÷ «½©Ä~“A _VŠ›œ¤a³6ðN¥ø!™¿cå_ù5í;]&v?„râc~øe¼/æe%Z¡Àò@F˜ ØWô¬ýú`²¸s«þ“ÝÁìíŽïÛÝÝÙöom3ínË?$¶ ‚ëZj[‚€v‚…¨lî.5×YPw?¡:=Üñ¯Á§ï ÈW‘{«µ>–jiO‡5@@±BãtöÁg!ÜwRýîýí2Ÿ™qcÇÇÝ…£;Å='?ž¹ï¾ßögçþ¡×ï_ÑEya²¶ ü^3ûCzHØ­0'ðç„( *Hzº–úÙD*„ÂW¸¸ñJÀÅ6¦¼c1ÊXÄë)1‘ÊÜ+Y²Vsë…2[å­â‡ìt»UƒVªs ±H ÒDj› ››š}i àNGêÖã"ÃQU9û¯{„z_øÈM2wÆãD•ÚGÀâ, øä&nÆÁ‚Öñïy70ü瑾æM€»Œ u À‡(³ëD‡Ëµ6EéD¤íTÕ¶Dw&ü÷ÄÙU›R¯onR%îƒxàákÓº~ž^èDb”T¾’ÀÜ}6óO¹ô "ŸP£"®“y&îÌ8§ÿÁË[æ¹§íBÇQ ôœ¥Ý/ªÊà†­¬®Ú^¹l°õŒà˱ PoÙ×8™ò½zCT; 驿A‹ôB°)¢apRFÚbŽÆ.¸Nª:ŠîPðáy.ó̊ͨ%Ã’½ ø ô´ÇGΛ£¨D”Ä%Áµ&|WÇkÄ[©ýÜ2U›sSít+ægÏž²só•š]àýv^RÓŠt÷sß[´ÌÇûíY´Ö©ÎŒK€hÓÜ«òùçr9$[³±dß,Io…N‚¸ŸëÕ"QçfªÅƒÚàŠÛ\J ñÝkå2ãçe1Pý‚ùŸ}{É¢el‹J657·®çÝÆtlgúÞB"Àá[±V¨e:õë;’A=ʼnïi¯à¿ ê.¡ÕUÔ./Ð%Îë©g–íÜ,³ ü˜ÕÜ~ô¦Oxa’2]Ãà:ÝJ¤¢ 'ÑDzIäÞícA…¸µõº!íwÕn§ûN€÷tÀà‚b&çĦRÔ(w÷’1„U‰2œNèq*(üEP[C›p¸þAð&yYœµ«©„ÀZ¡‹¡§@¥Ê™šNçɯ¯Xt3¥ƒämÅ®Á (×Þ8¨K‡³&ÒÚÊ\Ã!Cb@V˜è8Hêoø!YÊÝçê;]Øèiî¤` Ä7݉ŠH`º^±3À¨y¡˜Á9m¼rÑøÏ=÷ôy •Ò3V­ÿ͆QÞgÝgæ.ŽëHzý &†Të\Óâóå™áÂ’{¤M³ûéŠÁS—VNô pe§’ïWï¥y`R®¦nÄÜÇ2m/´T«HÁ…2r)Ų4`]”Kž¨×Áà>a»PÂR¶>þäÊB](óâÂüì1øÛqßâõÑŸGÏìï¾ _4W÷ïíî½vízw{{ϸf^7G(ùáx ØPöË€I½Ç˜âR]Êp ¦«XM %¨hjáo:jíW ÂgTµì¨•a´s­}ˆÒÉGö©´[B]cÙ¦µLp$™q£ƒ¹ÊŽÛåqL«%Z9Žªbü¼Í5¤¬± §Rè=N* +ïùÊÅ530\‰+qc+(«––©§²YÇ4³¬«çÎ-ÚM¢¬Ù$˜ká3«€&Ž3b>‡Ì1`ˆÆ\É}R eƉ ŽÓI{1´B}ûŒŒ+× çí+¶Ð–’KPäYŠÈM ƒøÅËo˜,oo¢Fv#'-¼a„ë„Z}¾Rôió’«4ÔPÇÛKÌ™wu†£HíÂãªÆ¦Õ ˆœm>÷`#Ÿ_“’šlüÆ"ÏŠèä•G@VÄ„¨XÕ‘YS¥28Fêùg– ÇÚ5Ä[xÚIVìtì•£§·<®y‡5æÍ®çÅ× VX™É"gߥ(󨕲ÂÛ¦HÀÉPsÀòAü‘œH1Ó-9ÙKo®‡ø¼.Š• ª×6î-¨Q/ã,åÇ:6õ&+ ï†S£T׸xíêFÆîp•¬ÛœZÆ%dý±1IGFH²Oˆ.SØ\"æÄã]4ûB@+îM*¨ }7ìJ¾z³jv¦ONgtÌ #e4I¼«dƒ•Ö–›ÎRpÝç!ê±×§eº·ffºoÍÏÏÄÁïõÐh`¾¹·[ŸÚÝ>¾³½ûàîý#7oîtFCˆr–×_…(véëÿöëâ|Õd$˜Ï92·b Žiz(è’©+¯^5ºÊ£’œç‘S«Y‚ˆ`B¢ÉJM4(k}‡ö¸ÛšgØiw*wò˜pxˆ—ècuÍë]°UÎ4by}Æ“˜É˜moé:ã û¥g*Té$ù¤â£n˜fåé ÓvÊ kbî'®ˆ”,¾à>ÍÎÎ…=)ýµFØM6ŒÛ¸ªÆóœcfÃï§MÜ…ìéé3²gË®êw{áȘ•`ƒ6„Gæ[I$ðùÇ7' þ„ [Ö šÌÑyÔÒ¡Áö<úy €Áq/ LÇJZ{åIÞe´‰Þ—wwÏîï¾¾·{û³×n¿so=if‡ƒq'/ß;räZ~áíîÌÌϺý™+ºÈ4Ù¹=ÂÊqŸ”ŽÆôeu»}kU V(i¯­¯ÂÛE:@§€b@"Õ Îû™•o¿Ô¤Z¡qn™f¢Ò½ÉäqéH}ˆ¢,äÈ‹´ÃsìQN”™‚˜„ŠfÌ ¯Ô­è–'¦'¶¡Šš;(y9âNY^¥çéÂ%x€‘2’qÑÉu’Ú¨ý–|ûÓógl&ŸPIëÿüÑ…u3n ®ìÔúhÃõ@Šº!µ!Æ´Úä1sA;͵ÔÈÒ1Wžláâ¦)×mÉÄ rýæù%ÛÉÜô³âëÄuk³áj\ZU¢²¶ìºVšF ·@)©÷EZÛ\òØ$ªŸÑí$9øÁWϦZgÚ…M‰ÅL‚Üâ&ñÁCßñÌZ®’46ó ³ ‰oB·À%ZØ%i»{éU^„×6aÂ;’éŒÐì2–|ÍLDjû½‘ÀUMʆ™Œ¯œè p‚ƘˆT}ó…k|üxLò¦Ô™ÈÞ„HÄ»ïNê@+ó•:›šHpÔ¾ eô°jiÙcBÆïIÝÚÔ«úÞŸAuOÎOŽ<°•îÌΰ˜ *æál<Åj=ñ0þ‰c +ã æ;çê W«Kr"óŽ6~UolÏÒu­%@6è²£oú…×ióÁdÒhwXxzwÒ¼ÎÅ®Ó]$­mBwIÛJ${©{;©ý,&ldÅš 4áëšT>W~¦¬ÌÑuÐ0s’`”¼¢o;Îßµy!˺–n¾&­"þ‡Úטà¸v€Õr?ȬfR1ÀL§Íé*\öÐ\œü†Š‰pžá Z›¾ÀZTÑ8è6p¯0À‡£ÐêÖÞÀÙdwËâøŽ …¢5µŒÒ”S­Ö¹¯Âq a— œ]7uh¢Ô râ`¨{Ôñ ²Ñ‚B׺VÌ=jÏäxiÄ‘ ÞºI‚’sxÏå•å¿ï使GÝRƒ³×ÕÍÛ7:“j²”ßõø÷\l_7ƒý€H;¶ŽÕÉFlÒvi\{~Ê 6äöIʬ“ÙµʘÖç9ÏD•¸P):¢…1§ÝQ¥ïY H£*¨iÕ` Z¸¶t˜ ½hLW{Γ4lKËË ëÆtzp´x'mÚp`ÃS¯¼|ÙÔõ˜Üɾû½%‹@°B l³×{J]„µ•Ï„âÊÏp‰Ða½p¯‚ˆe\°šW°™Ó mÎ…DË·Ó‰ïígœ î€\k%h{¶*]QØN¼'|jì…ˆ°úͲ,¡“MU²–÷jO òš ‹ë¨+åg‚ò;’Üè<¦a2X²F¶ÎE–8ç량(ÚDǽ&ŠÈ(ËÊŒMðh¤½îY#I@©tp¬M®‹KBw°07¨¡Žxü‘ëL”Ë<Jã¤mü$cG¿– ‰ó4âºéÂó­œik´dù,klöFfÀÙÀùu}Q׃xS˽óìì’ˆÅAÜFÝF=¡@¿²|ÞRÕî¼ÁQžt2´x è´:¤D Éiøœäp# áÂûWŽG óØ7{%{öÉod*¡’Ý´éTkJ'»iq8ã5#]ÂäÀ¤u¤&¨(ü`eæÃz¥ŽÀí¢¬%.xä't·ÞùƯá‹¥æÅˆBEô£óhgš7›6ú¾ïÂnIJj…úç·ÞÖ?ò0 ©‡yÄýóÛïjŸ­xà™÷ÀŽúÍŠ(|ô1!•4ü¢ÌÛoâr^òÍáH1aøÌÃ9¯}÷ãO´Gh÷àfÒ žT.ìüÆ‹õ;+§ì\éÚ<ß)>÷~æa÷óß¼£'ä æZèr×î­·ÆÿVlP#u&•J“ ÷‡ùø&(h?³i­þqvü/o¿§…ëkèy/Õæ•7L0øH>'å]g¼S;Ždz%cLhesKßðF«Ð²ðaWæQ9Ž*¨P~ãïgR‡.€Hz~î38œ5ÿÁgv<ôˆ{ë7ïjë";‚g­IwNÚa¸†y7cò8ŠÑ)ÿXY3¡7Aúª®¡laò{îAµ‡Ô±Nn¹ºsѱ Á%úWÇy SÏ<}ÊâïÝÿû{:•Øõ]/¦–iõøã°Öó¸™"¦`8)Ô+¯¾n¬ëAuÖ%yÏ\‚X#—¸I)Yš(?ÔÝ †ï8Ol âØŬæD@FøÈÌÏA`)ÙüUåj¨~õ«wõoà^œ¼ûG]• ªÙ—¶Ìp¿Ýõò"!¾Âî”|dI#œÖLèZN¢·õÎf* éx”3r·¹c38rtCrΩ£ñ:ÄúÙ£¬½¡’cDU;R™àT§Úó"àBžÚ†ë…ûÀò3‹ö¡ÇrïÂ=f ‹k¹²™‚ÅaÂW¹Qçž[±ï½ÏÏ2݃ûL×"á[Çjœ]ó0q¨ƒÆ¬Ÿo'- î½÷ÆÆJÞYñµ?˜¢ ~L¯TÅlGZljޱ\Ô†ªú:ÌÛ)©!™pl©þ³sÒ:79pƉxþ’?£±œ¼a'“ÏFžÅXlr‚˜ÌÂ}5.Zè.å ÃÉÕù¥%ûèCŸs^£ÑŽ”E½¥lÊë½\d¥³ðû™/´´g$AL±«¥¡1¿"O¸ýAj2­¸½¦u˜¹r§…çˆÎ£-g’ÊIûFª¦ëmb ǵ­óf«±o<(÷¯¸ð ß©e¨Òç¤Ýø·kf_1ÍÃKj\ š€Ó6\°¡ÅÀdÔ__Ü2ßÊ1ó6‰IéùLÚŤY&¹*Ãù[7žKÚ«–# Êⵓª oóÕ7b’ÌÂl½g`VÂïmÑ‹•ÁsÄg3+UõáXxÀ{ýWß^²ÿçó°á5£ìÄ>Y`:ÊÈÆVúA3t°ìio4s1Ái”ý½÷ •è^\Ít±0%‹B%žœ³•6sî¬P±¬Ú¯Ñ¢ï”ÅÀýêÚec$;¶‚š&ýfø·vT l£éÖ××Sæ€8QŠ\Ç ‰Õ¸ÜÇúV}盋ö¯áú¥ÅRê)®’@¥¤ôã‘”D*Ñ ÍdãÅ „8·$>£âì8T4î@`8(´ •B¿SS¥Þ-:ºŽ†gÿvªspá†ùÖsK–«ûL­Á½Á”•âü|“¼àÑ~S¾à ¼Ö…Ö:bQ´åÖ*Õ ƒ ÝÝ#þ$ˆ‹}'w8Mš›µÄ¾˜Tld àga@ù¥Mã›x¿L‡³{Ÿ3»ä¦Õ£g™ çñÊx*  zßøù‹«§mÞkT=4j ®—­xíÆÅ„ؾ6RÊãÌÌlèB¦øÿÌ©Dq0QCúНU—’«R½yå'f0âÓ® D$ïÛ)uNx> Èn_ƒ{\f,» îs Ø0õ!÷ŸO‰"Y«òÜ[Ö^ '}m›7&sUUßÞí”S_JÑò•ò(q’kš,Xß)ÐSHwc²„†9$èĮٶª['J(¨ ¨ËÀ4\eP1¯XGsêÈMÖ^‰F5qþM÷lÂ?³`'Ák}}+hXž‡½[·’«–¾‡E9VîÆÚ–PoLØ W¿Š„méjÆK¹©¹w68Ù­S™Ó %Njd.Ìî4lú¹ÖQ&Òê°ßyž'V^%¶ÔÅ×\O!Ó§M\¾±Â'À×£&ù_¬.ÙÿNx€Í48SâûÁî u«_‡ÙÍl! ¸¸ižƒß×:•Ñ”+f.Q[jÚ[Õ«¸KgQÞ´0¦Õ]ÊeT€|ð,™Ÿ+£á ¸8Æo-Ÿ² (‡éÚ-¼G=mÂÇýÇçíÿõÂƒÝ xaƒÄl®ªW0åF6=ON•âà‡Ï®.Ú! W¬mb­ÖÚ î”ryMj›•|?©Äm2BÐÚÓ°õ=¨4qãŸ4&lvéƒ?QE`“àýÜSKV'ê•ü{íó ºËZ·º¿>ã5Æ™ù_~gÑ~‚¹*$ÙæÔÍédÖïg®ži€=¢ÿ4r¥=©Õá…KŠZOå6ÿIÒÒCÉÏ*®ª{”ZíOt ÐÆÚ¥ËWLÕ”äN‰HZ&TLº‚‚îp,3}_q)—©eÞǤý’¡Àæ]nÏì,ÏþIÛDy<+"¶i©ª¥\¨‰UG†6^Ù„wE¨¢ÔS\—ÌÏgϰÐAëAb¿òí3÷,D˜g]«ÎAR‡}ý¦ñôBtk*up„æ’¡ï»A@$è8?û¶!Ù(ôÆ…–n ÁÛÞÝÙ.Ýü÷áþˆuÞËögݶã^³…ùyRÙЉŸ™è2$ÎÞ-uÅ2Õë—j ûrí*r†¹» {b(†¦©*œÆ)pó] ÷½pԮęƒdË‚gÚ¹t5Â@pnê%¡`8±Q+++v:‹¸xé5R^|jùOmQãÜV•®È¨…8N¶D–T"·m\&•²U*1¦µÄL‡_[¿lØÖ$ûVWž´ØQt*éf‰ñ«U6¢´g±„Þ€F'ú÷V+ljBM±ÚK_÷U¾ººŠF;áa¿²ÎŽ\™:@‡<@¬n/fF´~eqÅ^ØÚ2V òZ7‚0B7TÌê JŒ6UH ¼ÃŽK>ÿ÷ t K©¾2›^šÿ{6·¾i®[Ts¢TÇÜ`竹†gg™!qæ*±ØÈµjÙRM \f¼ö°§¹•m©ÖìŬôŒò* #²ÔÏÅúv¬æ³&t -;ÆùY§`oB€•Gãòxm·Õl°S |έœ±èJÔ΃9@þù·9˜O?_ŒÚ.#¨MÅy¸šr`ªEÒ‚•l@^éËM¯‚{–T­Š£Ušó†µ 4mÈò¹++KöÅ—yVçãF>“ùåkoBîÑ ª3Lq­¹BtŒh¸€’öZâL§u›Ë˜#*bjiKRƒ ¼ßP£Å2Ž|D›’4Yɸ"h@%²sÛ&k-z*ýØ‹S­@Έm'íâ¨!Ð…Ê]‚8|ƸRkkk©f$àÒµ‚á$#ï²¹Êôæ Iè‘Pt"¿9‰n«a£hT¡ÈznÏõçè™Åc ÷ \E@JÜe»†¹ñ˜ØbÀ¾fjg6|'Æ¥ª`#(%öË=µ-M¹ä.Á[»©ëèd=¡ñÌ_~÷¬}áÕË¥U$ÄêRÞT¦UÉ&d¸¾Ø¸cS´õ4Äåe àMnŠ.8 ¨SS3Xliç~ÜâÌè !ÇIÍÑJ§Ã¨TÖe™ N ­À“9iPåR “6ô±ÿˆ¼m‹ÖèV®[²©>AÅó?v̪o}{Ù"îù¿Û4M£C÷ÏRËn ¾ˆ]ÉTh]%µüñÉÝ٩±S —€Ôn©ëöã‰Ë©V{â©jx^ýÚñÖÒш<^t^óqa1ØQ­«)º™G)*gMíZ:©ÂN“`èÚÂß;(zÒ+è˜ûo ¬ÒÒ*VÙn'­y]/äBû`7éè4–vü(8*æ„cžPGp5ÒRGC®Ný[ä'm"7¹cXFõ0“ À¼¡B>¿JÞÏ›V–ŒK,èÁHUí)Ô¿T‹g¾jïÔ„'”ظ®Ø:R¨ZeƒˆV—ž‚;×b¤&DEC,Ï€²Ô €‚G‡8nB³™W¸`iÅúx-3×QçVÏØµ‹f‚ê‹.¸~¤òS+gm®r àY° S­÷(véNôÖ›pܶÈñïHÕÄó´tKr%GJÿô¯þ 5‚kÙÀR‹Ðà‚¹ A½¥ì¦£™ ^ò>\(”xüÆâ’}emÝì 5,Ø oÛ¨å-Xô°Ù}ûô“Öãü~¼µI”AÜ࿱´bµrÊü+3ó4î2¯ßíû2[ >´·ZU¬õkeWaÅ(@%ü`4 ° ‚=f¨™€ôt‚º÷V§¿ï?2¡×L)k ”´ËW+N@LD§€ þ­ÅSœPeQ§¼[Ìì[ªwÉg`bóŸ^ºjpñ7&g …f”3£@õëô´ì§à^A–„¹ÊƒÆµ ¨KZ…ž•¥×G·»6 vôZ¸ݬǦá 9êY̪É|¥>@#=‚Eí÷åEH~`5½paˤˆ­#o[K5úÌÊ“–ôÉM|ïTPaxýþúy”\eÞ|–PËÚã¬8=Y9xÄ]EcÏ<»BKn›ï_ˆÈë¨@—\.Ý覗Œ3ŠN) kajޝ¼y˜Üê|äh"Ô1´–+›B«Ux#À¯Š‚7lJOŸµƒŠÏ­³f5¢Ç[|ñtÞŸ|δ»Paÿwÿa íÈç?ù•.™áÀ¬A‰š 32Ó6:i͘E ow·QãQ“èšëvWH·ÍYtŸþhB0k”sQ:H,$$äI‡õö(›EMuÒït`¦öñÖÂç1 ¨è†U!jäWI¿Ž*r}'U8 ÌØ:šá‡AÜ$†Gíj\Ïš¤--vž/Å6ÿDÕí±YÆŸ6PDØ®QïeíJÇŸ"ü}ùÜ9ë°…^˜è˜D7*£Á¨n¸´Ð¼!s0FD¶î°†µ¡|\ƒ}üü׿h­q 'œ÷du“1‡[פxØÜÙ‘F;ü^ÓĸÜëf_;­qŸìÖ °UÙTªÏH â‘þWÃñ½)ü’1u‹P«‘æçWŸ†´80w‡=ƒ²ûTðûkk—M[†‰ÈªÄÏZCaÞR5Ž•>×å9Q¬ùaO|¢}nNõ×–Wm!¯A®¸§šÄ|J«gŸzÚžèuÔK®-ŠætÑÉÿh1/i\TÃD ­þ×Áo©{%?èÃgþ‡ÕEû}¨Ì'xâÖÈ<”q žÖ:.~äùb±¸j¡VÙTQÈK°òî)êñÆà5꯫ƵçŸéŒÇIeÊÊe•znñ´õ‚è˜å)8ÓÕ7%G4ÿ:ÈÆãÀʶ Ä~ãÕÈlK;ýU.Œ`#i†ÇÍDcBb"r瘙k=÷­ù_JuÂ\ç)Ü•ŽgÒ˜’[äV`ò_GÞpÒ´“¶é$Ÿ‹[¯™‘XÖ¦ª­KçX2ƒå¨âsÌ<^̓FDá ^h­äy õç´Ò!߀Fõ²’¯Z^Zµ¾õOmû?=k_½°aPçÜŠw¼Má®M{ÓSˆ7ÒÎy޶ ºNºd¹Ùí„„Ì5ÅOm‚8´’y6þY–„ˆÕpA1¯ìâ¼–»@’`‚3oL°ŠEou®A;üÎÿóŸ·ÌŸÿå’½ðò–ÙßC:·ÐY³àä®aU°`™ª’V°m_g¥D…ËÞÉ­0ÎÅ©©c£:BÄèØÐt-ð3­é²µË•< ¬X‡T¸6CïÆg U5]ª«àŽèéª,l„Æ)}º¶û£‰ÏX‡q­Éf—3*\§[êX¼@²Fæ ~F«ÛH#ÖZo„L¹ž˜$`Ñâǘ”Èá¾c’‡¦d@u:™à÷£L˜W¬Šå2nu»œ7¿ 6é¦Ú|(ã&QAÎ5:ú‘-)~DΕåÚú¦Q–ÛÔë[WÍÒù/[{@ÿà¾b!ø†€£|­&í$‰ÃÜûY…ˆq´5lB{[y†9 '¥ó¡¢>Iœ0Ÿ»¡„Á¯µB.7£õ27cßš”×´2ÊÜ!ˆ§÷ýÌ*¦àJàNÛéËT‰Woµb‹/Ï~€gü'P‘ûÄ‹øÍO‚¯ó¦#̇"F‚ë{Ï>mOBGjÖÿñ·Ï›Ý&VÒZªq®&T«ºEÐÙwÏ>iï„ßû×þ;,•b ÊR Zý ,ˆ‰‹ t9Âön̸åO¤’4Ä b»[Òn&ը¼eø»“FvK55μü(d¦Ï,>i í’9rTCYN€g2Ãá¨jÏðý¯Orõ£‹— %LÐ+[™·ù‡[µPÜ& °€Kþ= …Ñí‡Ä©Vûî°Á´²ùj5´Ô¥"BW¶Ò(/¢®O™¼¯ƒIbÞãÚÅ!™C™ßÉ$ÈlúCõncA]<ŒM¨ÄBVÛ`‹ŠÁh8Ʉӭ[ÎVED„. Ý¢Rß<Ö®Ãs²|~ÕæYÂۆ߹|yÍ2ªÁ‹0o§æýÖ´ä5Óå@ 0ÃëjeÕúMu²0¼ ŸÇp݇4´nË„N3šRzA=…”?À‘NÆ2¿/ûXV%‡Ž|ô¿€ÊÁlC“¶ïT2»M‰)Œ;ÈIz`XÈf9h÷QmKë¨Í Ÿ_Ogã¾2×í@¬Ð¶15<Ð¥t/Z“Dz«­@=5 [½8/ÔͬHÇX©|¯é5,Ä‘9QB'sx“â 5îX̺$¥sS¥vS{–kð<ˆÊWSø¹¥nH '´…å3ólú^óéJ¦Î•yTIQã•Ì<ÛЍâ笣¦Øp†{î1(Nèüø#]g.ê_Gž4Âï?'®óTûë ÛEHÂ8öGž)Ô­(BÀF ¢fÇ(ð[‰ò§³ŽEš[tŒ™gjóÐk«ð¬àq¯¯­j­»1W·ð^Ë+(U’ÝŠþ»vmÞ¸â×_£žèS+çQ† ‚öSðî×à}Ùo#Ûŵ ôŒ—ª—hhò¥¹¸¶‰:’¨É_\y¥ÍF^ég╬d?qÏ“j]Ó³’Oƒ}¼rÛa”³tñ§óò1¼x_úYÎÔ€3ò³g¤OZˆ6Iât¤E!‚Úp ol[E,u%:hÃz°¥Åêr*Ñ™ÓÂãÎohÓ·£i'$^5£²gzZÍ@Æ…M× êþ‹ï­Ú]Ø£v‡*8ŽQ"oyvqÕŽˆ?­ilÑÂ|ˆ|Oò2WýþL8ÝÂ[¸@5jÄŠÌO¬mà¾Ç ×'Tø'Îu ”o¼s•Œ&“šŒF)kOºí5àŸ!|¯ˆöFæÜ‘Qq´%hx•tiœʪaÍÝÈïæãÃ*¶×ëS ´^U°æ‘ a?êØRES ¦‰4A—¸Ý±“'h|8tUö½o&‰­f^·fœ žWmk/òÈŽfÕ¼j µÇ›pSyÕ¸&TÆ!`ûíUópμ´Bª%쥫Ì"5 {Žv·]«eêt¬ŠCuï³R¼®b¤:âtÈÁœ¢šq Áå³QÑ®ÐÕáAôA|õôÓöâÅ× á‚ötòV­®,²ãn–€ Z»°$tL'’Ŭmn˜(‹©Õ3KpÁ2Q\´Ôn_…ýÑ¥\Óà.ÿ¤îUÍŸa} rÉ'fÁ hÂ3<ü|¤‘)Ö`_ß|ƒR¡óËp.n4Õé”ݲ·5Ôë‡Jû)‡+¬¡bÈÒê ý ™!.@0ç0Ü ¼«Èoxt•¸oæ;j#ÍŒ}¬î†!Ü‹ˆðòø_ÿûÿ®xæô)[ÈSi’ößd üvX«}ú{|Ó—6/™v [€±vkÛÉß={ÊupÈPÙêeºwPTéóW®›ÑïZ±°…Å8ì¨9ù‹,aÀµcן@{ªÑ·ÃC`vîPn¿ŽpÒD¤`ºÝm} #T2Ó³˜:€Wœ7¬L³àpdÈ7{RÛÀOý¡wl®.^ºj¼Ê¦"CËÏkª§ŽŸ?@›) Æ3ÏŒÁ‚n¾³¦ÁÔ?4~|€Ag/W©a²J= q#f°Û”Ÿ´ÈHâþƒc e7ØùÜŸ»e l³ž€*yíÒº‰&n`«ˆ—š˜ÎEÙ˜‘ÊN ÎpK‘çÎN @^6!y*—öw ŸÈa1m qP¨›ï˜„•pôVñ=סg†Âåpø9øZÎ ð«b¡8âL%Ë$YÐ!ŒÇ“ñc0Þ…pîÔ8`Ϙ¾‰×njºŽŸ”×y¡^xí óÌ™'m·©Û‚=w¤î÷èѯàÚ‚®Å,Õ‰ÙŠ>Ô<•ñó£X÷o˜“§ÍmS¨ W^3&O¸Ì~óÆMƒªMÏÖc¢Cî‰.•ä‹\Yˆ§èdã´‰+› @N¼ÓÐ,^÷Fèi-¥x©¾­ú,€^nÔ¹ÅÓdê„f‡,#ÕMm›Ãö&ž)ëRm¼vÕ"iª´ 2ܶf¦†¸´Þ¯Ø«k ³X«§ Ôbǵ>”/å7>ìhôÈ!¾‘æ=–Ml;ÕòžOi]±eÎò½èhÇ@´,à h´Q׉7¹jiº[Qó²´iÇˤ îÕ»(í}íì3Lßa‹Æ:÷ÿÑ…e”úÑ˗̱£=J ÐNt{gHzë6¸¶·‘ø¡ýkSJQT.ó´·Æº(h§ÜTwJs+Úp—É·Ñ­ ¥qÖ;3Ó²É|Í«œdÃæ,m÷¨þ7ý=ž'ª™»Jw€S׺†)p-–"nSvÅ„E…êÕß«ÉxH.kZFˆr¯G˜$˜vGû*-°Å†§ßéIìò Ò7Óe‹Hñz¬¡J['™F¼è:pQ"1ë«rÛ$ÅÈ ¥ä€ïwqkxLV0ˆý)ë*y~­ŽhS/¯\³Þ…™5Ä“™Æ±ënDI•9êàèöNNßòüVÕl|Ck´©X®Ä+kÀ~³~õ"UÞmŠ¿qκaßXÔêÒWìjo˜T!ËÏ6±¥LÎ «Ð{ªÜ+nu»yø~>săy×$æ²9U™H­ÖÓà"qu)m“ùŠÜ`|GÄ/´EU2³Nm¡ˆfŒFM¥T˜?‹n/¼çÚú³¼ršU¤¢­¯¿AA܃×2‚aI±uHÊ™2‡´°UÒôÙÁÌ«žP’*sÏ?òï}ií5ƒÁÜ„ÝKGòD©¿ÃϦ3ûŸþÇÿ¡ð/ìÔõôغr¾jK¾ôÁ |úïx_°uɤ­yFWëˆ*xäÂ&øggOÛì÷°5ô¿à6ýóW/¿fê²æÁÊ‹øV}5áù:FéDR{J8—…ø:7Rq¥î5ÞÙ¶€wÄ9Ÿ¤oÕ>܆¿Ycu°t÷ˆæ3:ð¯]²Y¨e,p6„sgOÙBÙ–ÁaÿÑééòXRóç$j[þp† [»ÛFŸxßfôÛíÝúÓüøøBn†R´¢âäê–ÏoÊOð¸" X~JÛÃ>KEMSBàdºœçSú6îx4"°Sžþh$×0Œ+(а[ŸXj®ÆVšQ%-X'6,Ôƒ \ƒÁ€g½Òn.!ôK¾pYîÂsƒÔ³á$î9úP[kýûN˜$aØE8POu¦BAÓ0ÈÛ2ßÁ7êvd–Îß©ðU²6F¥’¾Ó×DO©DaË¿Û)Èì0ŒLcÛ|f?À‰še7WFëCǪÄ18‚ÓìOò#µ·g|¦¼%th–%›X°±ÍcϤûb ”|^¶yÇFQ*aA!gœ L 'äp‡‘PÍ2 Ã]_\¹8r¥$:¬+­.^Z7á–Á¯~mÑj/üƒ€ÄžÎ0þàY˜'«ªIô†jF×%6îu\L<øÆNi'Èã$IC ÑÓÕG£Ö/oÅpõËËÖ<®(iÓ¿­]Ü2Ië5”s«ç¾kIô=ì%xŽñÈÇpûaíÊk&è_‡V¢…êÿ¼¥ù9Ù& xÖ@P½<WWVìÚÆ%óÔÊ7-i| "ö˜‹ø«â óm~ÚHµ²|ÊË]WBÇ'бè…I"Û^Ch_Xµ3[\é¥ôÓ4ëÆ¨¹±¶iâNeóƒÿŽÕ¹S©†Mdb”8¤MÔ‚|–šftùT Ç,ß›ÃO rýTç«§ž´¹kVÀS€e•Ò{<qªºÔ ØH¥v£*z^7b€r§ÊÒýž1ìôÏð½^¹ú"b‚|fJ²R!kÊþóðÇùŠ¥á‹”¥c—YVæ]‚ž¸æ Ÿ£c]Có芨:ZÍ¢^²ÓÂô>˜¥DÁƒd6<ÄýùÕP%ïCp~áÒÍ*slMAH‹E*ëýlƒј‚@Eíq]RñbçA3ˆ­J­e”À‚VPÜÞA*q»NŽÿ 8.1¼.ó¥ìð9ÏB–‰á8ØÉ¹£ žMz&¯Õâòн QeIŽÒ( ’±Sn£:]ì|U»½¥œQúAØ'Êy’èŽk[œf¤ïyÁ‡!D¸^ÓÚè h çþ¤ƒíX¿ùþVª”,ÙH‘SQ´Z6w¦dwp ”d„ôî›F¥F-,BŠ1Pì^{ç,i£ã[@ÆT‘Ó诜™šÌApYÕú€x¤».‚X>¨Íž’v‡¦ÉÕAw]áÊêRÆÓÿûÿò?(°¥ªU}¡tê9èÓ³qf_ÁmL«­­K†išÝƒlÌD|%n’ÙxpiÂMÉêÙ³¶ïêÐY¬Ý¿‚£:d&>Ët‚¸‹Ài¿kĽç9]­o²ë£v¤¸Y¥ÞãpŒÏ}"™^%$xáÃGgÔ6Äq͉À.\¹¿¾°eœ:d&à9§Rš›ÀC‚[G9ÞÌ5//¤Q øeks“4Ã2œsš,+‰à×b ˆ^ê7Ã× âôž]µ¢d%Ì`L.¡Ð üÞ¥u“ ƒkmõ{Èú^Æ_Fª›ï>³dïêYõéÀ¨¿}y˘)T­ŸQkÀ!U52òÈ…¶ÓÖP¯åê¦ óµ‚4¥sªñ–O2.à€8eI+½5-h‹¾¬cÁ^ýlŒ÷žlaÏu­˜çDU°Tbµ/£ÅžÌ÷SÕCÿløN~fÚ= êÚI`Nø<Ö7Ç*ž]·ÚüGÔI@\ÄÿÏÞ—ÀÉQTÿWU÷Ìì‘lÀpA.‘3w²»I8Dù99•TEN‘á$ €€œÉn²¹€® rçΞstÕÿ½ªWÝÕ==›ÿ4Ÿa'3=ÝÕW}ßñ}߇Ÿpß}êÕ¨ãùn '!ˆ[!ø[›ÇNÆÑTBÑôRÂîm6t`ˆ k룹 4ˆ"ÏóŒÈ>ºx=±ÑJW·Ð¹qRMSBº)aÒšÛÂw®½gò½H|J’CªSÇ¡tu¦BÄ ±c”k¹ÁdNb„†Àg@ÜáGó>†Í5kžŸf$µ) `µ0OÖ£,¬1<0¬³¢nÑXtˉŒÁ­ÒŠc Åœˆ˜Õù¾ºVÊ̘ p3ÀÀžh }}Ë3Sµ&yÓ˜ RŸxÜßÒbyx–9JWA<Ûé­/jn‚ßó>$cSYºH=à?mÖGë‡B)Í£É·Ñ sÏ#Кún)ÔðLxo¡—ÜVëôVgε=I­E à|v™ð˜)&ÔtÍF׉×ÐC¶½Ý¹Sfsî‚ôÙ,° Š6Dî©™…g€7n(q%JÁÆ‹íÑtF"Ÿˆ%ËÌ·3ZfèãÛkXõŠ ‹Åê“`sðœwå­t©`5µ¹Ð ³,ï°‡·C† KcÑx©>ßž¹¾¸ml[‰é% œ6¼Ý£S¥0\iZ‘Úq›£"^Áz *Ù¾÷B1ýù´52Þ=&Òqº×¸­(À¹÷káÜäÌÜ\(À´›‡ç°@©f¸Ÿ}fº@~YQïóQ°¥kÙyèi{(B“1ßëŽZJ:d0Ch•ÝtoZX®ÄnÂÏ9^¸\€Ï¦Ïl£šåÌy-úæìYÅØÈ¡Íš]È'zQpG¯„î ÔýFàiimãÆ'—5ÑP5¥jT–ZMv)ãTJ'¥…Ç ÏKêCmˆµéx°àÁU”>Öùû k +}-È¢›»èn^í¬eÎÓ¢y,xáúDÒ‰”SÊdçpz(ëÔ„Fk'µŽI—’Mƒ@dÍ„ M˜ZiãXÙ2Û¬“|- M¤ÃÃå¶ŠDoWçÉyÈY#ÓgÃV*Ðĵ–Ö©¦ÖDeHå­]õ"ßO=a¥t©×'Á GCÂVS=®¼%] Ãט;õ}zP “ŸôBm«?îФì„Ù4j¤¬ E+Î6ØG‡Õ»ò0ÁM¿å{žš.zO ÄÄVÉR,…h(iò÷x|rÄ0f5"[²ä‹9¥RÒ8£ªVÒe0]Ú«6Gb"ʱT´ uÃ(Ž]ç {:Y{¼Óø¸“£u—oí5VöÏJæv?¼÷±Y'pË;ûSÃYð>r°|E]ç“,vWøCéÈ®‚ÒMVl®ß^ẆãËjjã>»Á5BẺ›:dÄ”ˆO¼4,Êdḭ`Ot¾Gåûx `È”°Ù'"ƒ†dz­çß{,Š%ý†zƒ¡}Ì5çB­{æLGÞLãkñX“•&zà&}GGy>Ü0ë•öŒ5ñ‹Ò|èl ÆÕš孜â–àÏÜ8\¨ž™EÞSáA$4…€+: ˜ÏžÓ*Ì}k~;s×è0 '|ŽÆ€ð¢óñPùŒÙ¨§ÎBŸ ñk €¯Ïãž¶$'»%ë»ñû¶YFÝR‘’èèͲ§‡x\AÜ»w™åÊa] 륣Ç9c:€y£ôëLßsÛŸ#ð¢^ ^ÆÓ¯b¾ö·¹اî8?¡!Z0ùw­ƒ®¨ê&p˜pp‚šššá‚×B™nô’g-sÍâ¹hÐV³#¶þ•g"/[åä Êã·ÌD2œgÂêŠB&œóaŽNšPzQ8ÝÏxÜá&™ßŠâ+–À“n,Å=õ02Gy €ÐæÆÑz²É¢hŽ_­­³¬¨vXÏí)ßyþƒÐò¤¯ÖH Ÿê#ooºQ3Û‹dÇd興”2-üíÞ'¬~°gìÂsÎÎ {#¡¯›Êc¨0(9€ω¦uS€¼Û4æpCëDdJ†ÖÃϘ™dŠÖˆà&G—•Ú&45†U«œŒ„"Ï4ð…ç‡ä1—…cóÁa3NäEÓh…üõHFmÜ÷ô¤Š©ÛÄ-‘±ûÈaÒ¥á:[¬gEØV€¸]ñM~²M¤&ñm¸Öž-÷ZÒجwΜ\§Ò©/®'AkPÙI:Ô®çID?l^ÒÊ¢Zæ2J3ö Ú›4ž±æËøÔn”1Ò˜'Ò ŠpdP Í”Odaf]±|Yï•gaÚnÈÝeÞ@OþJ{æS[ÚDÊ{ÂþÌDÄê[— sDJÅ=på°×-AÑ–‚uäÌ}|x_å°—3Éð2‡‡ÐÙÙiòÜk«ËR i}”SRø¿:˜ê²ˆ+§.Ù÷“­˜M9Ÿ‚û­³Ç” ÚÚ¨3ä}oÆDlTJ…†³?Ê÷1œïƒ—_6 (eT(%»h>\Ï\ÖxÿØ=ú®n¥sÿH&Bæ;‚7÷HYÊW¡’›ŽÆR Õrš¸HhI¨¸XLX*Œ¿%L@+Ӈ霯þŽÂÙH*ÎÀgmàýJåáÛe Ë1ëȸ@C2uecDu´r%R®p§Okš(íxΣ›¥GÝ*õ SO`‘…ý8À#‚ØÎÈ]›eÉFnœ»-å µü[)e¼YnÊ’cú¼Fžª‰3,$°Í|ù%¦âbìð±aŸ¬+סú™-ªvá¿U)kôØÃÚiçÄ_ƒ9×–x³³7êöxAl,Þæ1F±«G‡=©d'Gݾ$í¼EYu¼ñ6˜ÿ0Œ^1Ñ(·- ÉómÝH¡é°/e¬n[³IŸVÔt®Ü• óÉprÓ Ÿ773W›¢èhSMÈôkm-\SM¹;¸÷ÑÓ6â+˜›¶ußMšQî3²n,`ÏÜqC~SŽéÖö[ ^# ?ÿì³B 7¡/OO~…|O˜«ã,J‡¸,e¦’ n>Àœ¡mÁ6!HX`ç”[ÓápIº“µ æ * ùL9áMʺذ äˆKå¶Ë#…O`=:Ú°½ÆŽk\±¿·Í1²ÕX§ò$ßóÂ:^-Å7j¸ÌÊR¸>Y ¬Ë°B`ˆEøê„ ð/˜O5rKÙ_Ð19í.˜J* ·êŒ ¬/XX¹b!DL€&Ù¼#ÌçªH=O:zî8;âdУŒ˜¡šô ¡Ð ñm]3~®ëŒ©ñBÉ4v°aÙ5-×0A H›ûLQCE=“m9PÖ¨*r§3™rJäܰ¹rKªˆùŒÇ„ž$¹l&Qn‰¥¬\µR—6¹¥±J)^A*ž‰@O<—Iä¨m=•ð% W;j˜·w‘޵cƒjcÒ)§KáŒ%Ú¦’?âE@¦ŸåPNT†jdvœ†üeÛŠfáµ×7še6k@m–,£R°ißÓÜËEüq³·"ÞRN+%MŽWÒ7BÇÊsBÛ**óœ?¿Eàx, ŽdDË0ú«›¬øTBì„Æ-9Zñ(ç­)2í0N˜f ð% ³¦Ñ$ð 㑣×Îá:Íl!ðwˆØ8MŒÛ, }‰Ã•'C¦ ÆJ{´A†Ñ º^¸I­Mo³D¥zZçbv‹p}~ðÞ Œ€ŽÆÒ¼ï1MLÆŽh–èy9UF¾^†µNŸ."]¼Y`ÛÍãà7*± EC›=£™Œ¬yh“Ô¥L’G BÒЧ24 °C³`4éQèϱ™%0ª3Ï5"C¯Ùܤ5tñˆVÏe˜“F0Ʋ+[ëoBöÉ{Îè¢såÒòe,4•vIæÆø›4€»Ñª?×ùðHß¼¼^J…mHñýþ›?U°îÈÏ=óÌȅͱJéÓ¶2å·çeZYÜy" ÃкpôÖã]а¤F$Ù3lÍ_@'H+8!mN˜t°k‘Ø× fÝÇŽ–VçɳD¹Š8Áà V¯ÛÝùÊc@Á\оúå<^65ÝŠaªêÉ–V±¬$R½Õ(¬nÞ×÷é‹&Øè!©u¶ŠÄhl£†d<̇K7”nU¼dû+H£Pì ã‘ÕÖÔêÎMž1Ák@ û;O}Ì{«L–ñ·¨È… <ÊbµVLK*vuÆg©F‰û²Êd¶É‡N5Àdƒ© ³]wɰÃË]jÅ:::‰Ð—moõ|ªœUŽâ<µ™¸pŒëqIa"…Yƒ·Hžvë.8 iºKW2 Ã’,bG;ç.¿ çklwŠc?¡If2XRijËg´µtÒñ·G³¬ÉÄ=Úå+Ð37jg”µ²Ü£˜–OØQL€¬¨Êiytë’\ŧ]aüsæ¶›wFWŽ1aIn–¿å¦T šÃ¹“­Ð¢fÉs»cC0Ä;¾ØÂeHgL5Qfú~èGiŒG÷HéCùtŸH—U$ÏÞ#Qµ’í[Nç-p"’Gd»ðçµ Óü]ÅÈœÆëVlÌ.ã¤"‰Õ°žÛåt0Ó§ÝÑdDN”‹ÈðnÔÛx<¿§=‰,1‡¥öSøÅÊ­='„`'Ÿ(0W.=Ç tŽL̳G ï)Ù²WÊáÁd†ÆH–]»öQóŸ°sšG–-F$Í:‚÷nƒD%wœÒ 9VW_G5½ñòZ€Ç'c7qkëÝ£úáH‚Óí0VÒeÁ#ö¨M¢DÝgp³|žrŒ””ÅÐ9‚^œiù9JkÂþ°?25£å12%ca5¼5 H8‚&RÆŒ*Ý DBõâÖ‘»‰Nr/’WÅr»îîµJ„ r<ãµêü³¨n·^d`$ X}-–“4É\FÆH^É–³yðØ>Zá(c²HÑ-jªâûâyÌ*s)lR§M à5‚rA[#àú¢—Ó¢»a76?ˆ?»+W1ÖÙŪ®f²ÑÉ8#"Îd8¡(JPJof„ëaY• Õ#‘«µR‹÷³¦q¢Ñ4{N‹öéFŽn–™zÆ–|yá “X@jÚ ÚO ÑÀó1~«2axÔ\&.¥÷i€›µXëÖxð¨/0¯½G~úiév Cé*$syäM‡éä591Àw:èQûRë‰G@ŽCMMM-”¯AÝxyN…½‰%:'Œm-a¶ÁNeë"ˆÊ´ÿaW—Œ‡ Q  QÊ5Lú×®`ÅÚøw†`NN*ui6µËUä¡ÔkÆSNkĸi£œªt:YÙTƒ-‹B­jûÆïÁ]Ò*’âž(pÝD°ˆÞM2§,Æàæ½E uM2¸yBˆTå1–À‘´UË0Ò ¬8 ]G]ˆ© 4IºÓE¿R%½nò‚4È&•!éTIJé”LÜwn†fã F·}ÒråŒØâ˜kÀ^w}Òà‰sk¾˜bÜ©¸ÈÊòÆVuEUb¡*'p¹ËÕö¢f¾9زM¡tÓ+˜;ûö!–8Ö©Îm" ž`Ö‘.‘WÞ§/FÏ"8†äI‚]ç¨1úŠÀ× Ç‘ïvÆéœ#$§ÕÖ“QAÕFH¦ภí,Þ ¾{fŒ ö?f<Œ©Ô¥ï›1Y¦zè}'Á›W褙òW&ÂÐûã”§ncråHàžû]Ö„Ä;TõÔ!õFð†mØžÂíHžÖ/'-¦;œýµ¶a×-g YÞÚŽ1 '#~¨(¤®ACE ÜŽ®±ÃFïîtUЛ¬un›°u°J9ƒ SÑTv ám¼ ÃìÊ9áöBÅN¶[¿W¢<·-)k õ€ÇíPò“» ›GnMsã(Z?z’&<âÿkðŽ=ó­÷ßëåaŠzbúŒ2_ƒ;V$%)1«'ij´b©X(R'¨b˜Ó9½¬0æ‘Ø³ztP©ˆ‘dðZvn ¢Ö‡È‚ÇÖ¥_Aïî=zÖ‹èyº²BE32¶Qv³³«3ÞS™¥ç‡×È£.iƨ©«­3!áÄñX"/âÜ–' œf.’òÅú`—°¬- 'ÓRA3Õ%Ì€^.O PI¡ \·³tzG³5ñdé–a,™@ñxÙ–[ÛN¢,‘@¥º{b¬Ì*S‘gÓØXBŸ-í‘ð¡á-H:¦À©áµ7{‰Ò†¶i‰ îg–dèTÓ˜ó8€×f%Ã`4ºÀm—DO,ÜgžˆZ©áfÕ ¸¼Òyœiå}©Ä)ù çxaŒ“úZ¦[‘*Êc>ØË‚ÛÌY-b ‚¹3¿¶Ãc°²ô“¤“ƒvv'¼™Wañ¬x·5õÑçy"vMkÀ†}Œ!¢™nx”1ciÞ§Yæ º¡4ŠV-'°ìŽÓÈc ]x/7)òõ£|Â/83`¿– €œIN„hÍš/˜è€´¢_Ö)´%þTºlÇLr•À¹&8¿Ú&¯®ˆËe%Sy$ÅOÕN&¼ÇÚ8 {'Ji]¬’ÇlIàдΙ.Hö‹ÈdÎ 8@Ž©Ò<¼Éreó³ÍŸ—¢Ú=÷ µÔ…Ú9«Ð)#Öîtâÿ©öš¹¹_8{À<`É>º‘,¦O´\à‰ü/£†X^$Ò›­!O†Ö1·™ «»‘+œââ˜*O<›P:åê³ àÌڞÆÁ ¸.;òãäÓ…¥X0«àj`¸¼T,V,ÞÛäÙË¿£¾Ù&?ŽåQ®ÇöLÀZà°™K܈åÄ ´­Â‚8†›kt»GAF5 Ý”òš'€T_AqWK±×!g-ÇêtcK3\x/¸²ÓaÜHœ‹{=ªœ¤EŸ[Nšv²UÄ>W”nsT¼2B:ç-çu%ݱ/@ÃL„ ,m½²¾­êw :ÃÒö¡*dp#hcÎY·‰¥²À µ£÷­#¤Þe ’€Y´LãDÏÕZoä/luŠB1<݈ÈYi)¥ÕF¯h%<†\Ö„Ó³D³iÅlÖÃŒMŸÑ"F¥’\ülÙ cð joî=ïj¾‡åaTSŸ¡Zû°ì`W" ò‘ÐX- v!¯~Sx)™è5ŽeÅbàSýTVOü­DZ ¹,E0ˆ»5c~‹ˆˆº¯ Þß¹YVÉg1±˜dš8fìˈL­ÓÊ9£b׊ùy5<^ââJê'FüØaã¤F|ÞËÄä6µ÷)½„Ž]Æ8ÝpÔ¶3&.¨Pͨ¹q¼ óߊE!o•vBr/ýÇö2u¿7ÈÄ}ö¨‚òÈ#µ0­™kê±%#–cÙRuvßÄM¬‚T ÃÔ¶ƒ˜pBëÚ H‚xzó¬tò÷Ã…=]RšË *‘§VCáR­[ÝÚ"P€Dç•‹&,«·E÷ä¬6£ Ë,€[oÕÁŠ’ñx0¨ƒ÷2*±Yp¶b-èy{È TÝmIÈÖDF‹ëÅãóÿfŠ|!`Cw-Ñ+·ÊjtÕÞlý{D²Ó‚^ÊÑÌñ2Ñ)cBÙYû ;Œ¡À zïF3Ý)¥‚õ†k–íKLp©Ò»çjÃÆÊ|;`-\‹ÝÆ8¹0޶y-ÂU9UntÁ)»SôXc_S»ÍXYwWw±$#€5SS>F¢gÂ!¾¹°Ìî ÂXÍphyS]Fe_%‘&¸ÛNX9Ö¯`McÇɰƒ™ËΔF:sâ¨)Sç½Æx’Ò`LÜk¯*XÞ@Þr9<1ÓŸ™/ô“먈áz«ÊœãrGÊm ±d¨6Ūiîgn·­2K”þºá<[/Ž^w W\9Ä*Nµ®(2"jLá‚ æË{dܶà)ÈK?¡€Šf² :n•¿9ºÙÉÄReÙ%Í$÷x%ž>Ê9×RG‚YÄ`§‰´T`>˜ýZÊžÖB±Ä:‹¦C[’@Õ©-Ù,C3êé&h/X{Í::©Ð)X UÃ<´Šµ®ÍF$/”ì.ÉÊ·ž;Oâ6fP–È+>¶Y*‡¨†¹`E€¨ÍV¿XO7H?6ëíæ|'_îEÀ. nVÎSr¢F@CÀB¢V'€ÅSOµ÷<Û®“6ÜPø7ÌUèo@ÁpùrC,+–"”¶9`€tI^¯M»/k„¸Q›ö³é‘ŒÈ½üÒ,Q#¨»'Ð]éðÙÙ¼Aü~)œB­ñ™ÆA¢\DÐIé7 XÔ6vÖ\PŸÖE2\ǹ Z¬­«Çˆ@kXÐXÈwÂ1¹e#,*‡ÓF5’ãjÍŽB-*7³zë!÷À7d¿“bóÕNJ×*~Ú>Vn\מ22éDE’àÔ§Ü–¡aÄA‘ŠåxaŠA…ýçyh$èJ ‰É²5hûÈ#’³!r¦Á¹iìxÒ”óôP©ñÄæoUAøËä/>öˆ÷—™s„p•-RmñÜg*¬mtÍW àš‹L’ ŠHqªç<ø0ÞÁ݆àí«P4òHJU¾‡ËØÏâhscËÏ݆+-Ei¬Öd­ÛJÍyc=>Ê[:ZÑNm¹&ºa¨8›Ñ5Óº«gÔØñâ‰ÙÎÐ|Gg§ÖzW*®žVŸ*ç­N˜HVR1ÊôÌj€œ™FCºû«˜ öŒf¶bñžæ*jæÑ›mj ‚œÝMF0Æ×òºiNõúØÃXQ>Ÿt=/ÒNÇ¥«d&ONÞ5ž mcˆµÔ1b^c‰<¸YDCÇŠÕêi †¹íÅȧQ Cg‰xåæ9q"í)E R¼r¹`l&Œ Arè(q2,A7YN+Qbe’T0WŽ1`ǹm®¡ ˆgÉ[“)¥”HÂC0™ð!È ·¸Š<ìdDÓæ¶»º&¼SK)…ºúx˜›³øXñvS+U46ÚPÆŒ1"(sæ˜q@œÙf%¸_L `IV®!Ê?£GŒ ‰FzÉ’@Îjñ„uß"aH±¸Z.XÏÌvj–rÙÚkÍ´/˜m†D4—eE°Dæ}@ Ê™ ±yŠçE)eÅ£T€ò\Ë6>KÈ [y;^‚;ŽÐé¥s„b;¶cš­G9XÅÊ£ÕHŒ;xÔÄ^÷¾¹÷óƒ‡TåêB@~ÙO2ºžZ)ÝD#Htµ®C—éF j àp·×èV9&±;Wîµ–¸7,ÿÒ”¨.K×nvÓ„3!ö¦‡ßÕpÓô'Ä-nžÇ€\bóywjý°jÇ9cz«°Rû¶ÝjÈAyQR!cä]†§#¹¯zf{¦«P°dLS“ÌÁ Ò/ÃB>vçªËF± Û'|õHå;+„ÈGŽm–óg· yÚ¼'ï%¬®¸°µ§ïÔ@ã~ÑSÏúÑ$(`D‘ªÜhsÜwDSÖéK,kØ6 ±$,ak…ÒWžÅ5‰,TëK‡0æûQ¤!­º <œ…ýì­'‰9Û’Š¼B\1žë‰Jؘ+C=ö*å² n·«Ó¤Ü1BžŒÚ«b„Å’µèM–êÞm4„G=½ñ‡¢‰ŠO9³›2ÑàÝCênd¥xçIî-Û·¡óâ^$¿- <©­K¼fÓŸk£qLaÝ3m‹ FtíñÍf €ùq¡ΣÒ.}í²QèN2|6}ñòmÎZ±r ´½P:¬ïܸM#”«‘ájWÁ»ºü‡ùo.¹8ãrÀõdªÕ±¢ÎaÊé6å†Ï­ .Ø1 ½ol¼¡ÊA9êmª—zdκº»´Gn»¬© ºàÆϱtidh݆I3kèùÚÒÊ›KÄtøtBÓX‰ÌòLHøŠ€¸HħÀ1´žÅM-Bjê˜[ìu»P[êÂÜúüˆT¨8 Õƒ‘X¦A›@Aº5ètÚs°rßü hÒDB™O^°õ†q ]I!gB*+·üÉ9»‚W®U½*¥œñô¯1 p)£±xŽˆ”n,£hD;€@g¢–Z%úšÌŸeˆP»ïÞ,ûÔšöª¢9÷ËW`"îq»av«Ó­Tz(½×0¦ oë¿]š'îsÉËCÁÚžʱcI …Î uÿ~Ñø}/=WXË”Pš÷ØÛöµáÆ‹Ä4±ìv-®hïUGåßkO|(8­„m¬k"â ]kÁôø»Ä|xì¾rNŠ Ò°¹Ï¶?÷ð-C÷n®ß¶8UTÖ†çsѱ‡Ý‹8©Ž·îÅ…lX²U©ï´Ëflµ¤åäóQêêò_öÈy(ʱÏ5¡‰ÂÆA¬u¤Í·mHÀy(ʤ¸·¶ÿ¬3ÅÄЂ@žÊ|¡ÁXÊí/ȳA¯¡K–®ÄRÂì‚÷@,a,`¾x¼vÓ(³ÜÖ;sFb:GK«U$\rý­w®×ÑÕy6œ·oÁG}àܽ*8ÿݧÿ€eÕóPõÌžË'š "ê% “µ–d¦B†ê®-ƒØæIõÕ ÈÈÛ^I¬ßœ‰ždDô{Þ,…o‡Žh–mmÓ–Iåóé½ê\¯Á5ƒŠ\ifâ’¼LÛ\ÃY°ºîä k²ˆŽÇ1aâx=º‹T ûXÀïÃ%‘ÈŠ½ý¸“GÎQ)V¡d¼V[RfKµ T&U Ä­'nÏY%OÜÍ£úNíx‰¼Ü‘H„šaÂÛøWƒ9Iddu[Ñ *ñŠyØ”³-#A4¶FÆë…‡]b¾×9}RŸc2”ÆÏ£=ï¸ öä£}aé– ¢~änÓ¥â-n'6•x¸,‘m.x×^ÖÉ…;ߎI· ¯Ö«KhÛ'ø/VÜN+*mÄõu˰°ª"©o‚÷IÉU?8o Oš±Çõ­aUà®.Ÿ±G~ÓO~œÑác—°†!?Ý1 ïgò¼© ‡$P׳r¹0W6Qa½4"MK";,ôÆÓ~h’åôb•$­aû¬…ùñ´Ð»ŠöSÇ%kjj’5„­Ñ+ï”NH¾Bówÿ%Ê_;éŽÁ]Â?w(3L¿ø¼SN¸|MrÅTÁX\A A¼Þ3 NrÔ|"ã±Ôk£½rèÊB”ö°ò¤6ìm¥ImI*Nàs$e-'=Ê °Ey^:Fq¢› êxíP<å{6ˆzH:ÏËMˆz‡]›%Ž­³‡•id„d04ì¹AÐò¼¨Ù¾Ð ]±2 'ÕÊ$Ÿ[’Æyy3{Œ¸Žæˆ¸'0--ºcLí80žûÆFCÇ›‹÷!óë´~‰´Í-½Òä¢ÏDú iBÞ£áX¤- “q°·cøf5W]]þ—€üæ‹/ÌàîívÛER8O¥c2$´Á »–Õׇ9Ô#6K}1g¬å\àMý Œòø¼Ï›õ≇u«¢|@Ö6æÃÇ75Êzp Ùvrp<‰U(Y*ïr–zô2DOôòßýþØÖÉV—ÙLfÄN:æ¹Õm3ö×9x~ÔV# ˜»avVÏĉHiÛ^Q0%i‰¾ é“2®µ¯<Ø„¡T4q ^¾ýÞºÛ)å¬éÆ»D£d÷ Írþ\CvS<"™¡gÞI¹aɲc! o¨w&þ·…¿ºz*„Ô d|¯Üó¤±nÇ£%M•)çrolÛÞUæÐhjl’¶yÒ ·žtc€WJw/žžÉ4”Š@\Ëž’gãCO\ùm¦#Hc=üyÞ™L9ë|ùnêù(go› X$O–±.¨DoÖlpêùô„yÞ̩ϤwÞ»u›túÛ6+ÊU!R¡J™<ð¸÷Uïêò?ZG-n¼Ó³!k=ÊãßR¢ŽJP8]‹¡®ºû1žä°õÆßfDj“¡®±•+¶!ô\â×iaóD©§+Œ5}Qf{¶çW>CàÎjWDD2ÇSRñ0 O}…ɽuÈ(G)Ëç:ÓkÚôr D ƒ±pΞ ÇŸèMPivòÎ7žqÙÐo¨шtC ßÅ ]º¹ŒFaz{Í?ÿa—1>Ö0âÆáV–Ê`5q°µE—ê£W¦Yïð¿‡7k‚!6ÖÈÒÅÇFXƒ^tB÷1Ïëç»áX낌 /äYvv—·YµÑ Î#~s~g½åZ§™Ÿ–!Fb0¶¦ÞFøBOóìèõvŒÈŠLH•¿…¯„7¬TúK>YÜÈ„·à_¤&.Xþފɸ]Ò’¼úÞ†ñ¿ðµEÀuÊéÊ‘ÛÜ6w¢_xl)јˆÊÅðè%‡%d!ÔMhŠ€uê3SâºLÙÍæéšr¸Ov¯æ¨«Ë—Ý#¿î‚s3–Ć¢òØòQFžy!QVeuÒ•«Æ˜è¥““ÊaŠ›ô,s{ „¹Oò,Eé¯%§â¼€ýÎ6æ1¶I#±šFÁÌ䯫õ"†¸žäDÜûOÎv\ÂñÊ1ï½"0!s÷í¤:{†)wÁI­ÓëÃÛžYpJÈ_²XCÞÄEà|~Æ÷O?ãÄcæUr6’aK›·ÎR”!Ã#"Y–<¤.ªçÍxù>±›X,ϧ×AWB^•¯O¶{í­ÜËŽ7Fns<\ës ÷#8X¶6£ï1¿œ%ÒæáÑ›ÖeJèÝm–RÆeðºqo;ìõ+,]V^!á²”Ãrº¬ O[å7î¸hc¬‰–§êœ |Èn§ñ G\,%¼ …°1]cï)8FFò<–‹"ÆÖ³*v¹Zc\¸–°´â4ŽÞºe³c^Ý#"~¦\Ä…'ÓVNHÛŠ²„é1ÖußìÃt,·Në»fÀ;uþ”X›‡ ÕÜtuùÿȯ9÷¬Œ•µásï<=¨Eëy .ɨÆ×ØÆ&óLcX°µU`n™Ó“©ÜfȶV¶?®\ï\X^c_a š@úg°8«®{e¡Ä Dªó¹K$Vamtw‡’WSãÇ=Ì’[S½]Ÿ—³µò²£ ÌçS^Óü(2VžXðú)ÕOàß#ÖðZàÿXW[{ÅÉÇùž;|á±…¤3›j <"ÕÙ™¬> £àÄ^$`+Éôºi;¶Ñ€ó _Ð-a£msžw{5ØMË”Þ1iFG††Í£KG„Å·ªp,:(ß‹ }À±G^lWÁ¡G^ý0ðpíñã÷ë­c.l±Lû\¾®i)n`†ÍuœãACB×y+VV’¦YÛE°ÌpŸˆX±뉺IJä1ib!)’J‘XN%ËÖ±ãv²¤Q»¶”ÿžIjbn¾ÌXÃ-ð¼x[YÎÑ,§¦ZU`6ÚG&V÷ícŒTk“¥”¨TN×Xõš«Kuùä@~å™?̘–ÔØNowÔØF™g†×MºÙÌ…3y[/ÚNv2êvš…ëy,®nXV!âÖÍÒ„¾NÖÌn%ø²½ aÒ2¥WèÍy´ïu¡ê œ;)K$°¤†zŽÇ™ì^/nËÊ0´ûÔ´ÑCkn60ð»Ën¸uë|¡ðc0Œ+; 5[>ö<ñË ¿²þÍ'¾w [·è¤Ð§íªd?gÎø”¼Xr6¨Î¥1,5+PT{…2Áf@ÙXS§Š¬„5éaDtF:úé0Éu’Þ eÏ¿ˆ¢ –YoDm(oËÊ=|Ty³iÌ•kÖÞTZ„ ‚@5ÓÆZE®å’¥ŠJá¸sÿ¸ ŽÛZ§<¤ ®M}¶õ^Ãî_<ΈËè}Æ‹çé3ÄØ=-Œ¨(m¢‰tØC¼±Y†´Ã¶†CÛŒHüņ¾1Záù‘1c×µâ|?QÊF'}ìO‡Õi ÿM†’âš),Þ2k ·ÊncÁcž1w [­›®.ÕåÓò½Ïë S°› >é„VJÃÀ¶]f…ƾõ HzhÂ÷xÈÓÊ9{뺥xyø×jT3gb<Ø.(¸^j­‘³\foÆîµçuóîö½5Rz´ØK‹( n‚ ¶}ÕÿÜo銕?/ü4X­!1úw9çm0–ƒ!öpž_‡ÏI¹6 |O\~íùÇÿÕsòñ–øgØU4JhnôÁ¾×=¨}ã×xÑù²-?uê„JÑpø~þ¬"(²6ÜS”ÚXpS 8Q£8Œaæ}1jc\!uÀXŸa<‹T‚U âêoŒÅ÷g 9]ê•·ë´ 7w¦iýi=zÝ“¼`¶oÏAhJã¡kð‡ë8Šê­Œ€‡2¤¥€Û¥¡o/P}³®ŸEùfõÞæ)]ÜÜÜz £û0dÀóôÊÄí3€ÇŽíN-‰,™/½oúý(ê¼&D|ln„@éûêvþ¼)|h5”]]ªËÈßøËŸ=ÍP( ÕÚ—æ«d\Mðr€æä‘åYå^÷kòYL¾0=’®ì–©ÄËÖ˜ê´ÓPSÙ ã  ¨PûßPk¼d:hÚó‡×nbϾôŠx¬¥í¨@Êsa!‰áá'·Ôä2¿¶ÝïÍ~éÍKA© >_ÎìKë\çÑã¾sPÇ/®»eüþRðâG”ƒ $—ñ/¹þÜcØÜx²ü =ÐŽB¦y½(Äb¯•>^Ä`wk³1l?u*—f, 0”VªãÍp÷9b¬)— \°ãéù|ÜOC]Ô‰Œ; f=Aê¨F l(9¶.å}Ý´þþÙ9ÔýÌ1NŠd”äéæ2ññKò†•ÓŠsÄèè˜TZÏõ‰3À;ãEŒláä’ŰÄÌžw- C9rFÇ6òáqàfNôcÄHC‚›¿Çp¸%“Ùu5p;¿³arwÝd¸Ý–ŽøåVÙÛö?µð½‡n®N¥Õ¥º|Ž@þ:¹ sÒ°É_œÇ—cgܤ–µ%ÇÉÓµ€Wêñ­øêA¼’–z È©ö6祗8q‡”ÇUe58E„=¬a¶!fò¶WÞV,•®›À­ß?3™oó¥¸è’"VNbzdÚ,ÿÙòÇðÑàÄæòžà7è[ÿ‹_Ÿqø’°¥³Ê­–H>ç9yWG±¬†ãnt QJ9ìSÈOˆŸ ~=™ïmÑwðv‹¾¥£:V—êò9ùkäˆYÈ-:¡«!)ô¼L™>Ý8“¹}O”ã)áö˜GÁ¢†$ÖÛ·ŽF»×lÁ·šrBá5¢÷–¥•„d®¹sJßÅËV쯘Ú¼ë¥5¾÷ð ¾u‹ÿ½tÕÉR©Á*ë%~Ú wÍ€~ý®ùÁ‘‡,E*h<3¦GöšÐ,ëaÒ_Ù‘GÒ³ø»‹÷Ç]ÁÛGÒÜ~)Þþ,ôŸÝyÛ©mÏ¿ÚÁpÀ˜_ï(ÅE]X €‡ ËãÀ俯k‚›JŸ20§ß_rÃGŠÅIðvkgáµ^Û'¦r¬¦»+—Í^qÖ‰G¿;nZíŒó/Û4_(lÈ`;c(<ípßò}oaÿþ ‹®¼è윕ëá]tÍö/òxûõ”Óñ"KȾ MÚiÛ-N=úÀ= ¶®¹³˜ *Ãòöœóá²³‹$æáëQ‹óPr`øXó¢`áz·ewVtH8³¡fÍÔ–àÆGœ~åÚ÷ã‘ôʱaŠ[¿n=cÜæ÷1?> ÆX“J¾ÒÜ\•¸‡’ýÊ“–’Rò^²!é€ù¨¦5Ýyð–Œ&!™aŸƒ„hæ¶ý.ìã×ÜÞUÊ)©àNû¹Ç½3 ß{èÚêtZ]ªËçä¯8@Þ—õ' 7]¼Úuøœ“JZäMcþ¼C•ƒwZ^A¼^”{œú©ÙƪRzk͘÷Íâ¹Ç’Õõ&¹R[‡m_ù ¥)‡óþW“î\weGç3ðvÃô5¸ë·ùžwÉY?8¶E$í¼K®XwåªöÁ£>^Âõ½\‡`[Od|ïŽ[®þY‹û"·ÝÿHÍ¢wÞ;):0xuUþ«ŸŸyÂam¯2ae‹4y¶1=Þ@FÄ>á„RL­¾ŠŒ:ø;<×B)å¬ð áü¬¬ÝiåjëÆ=/:˜˜33:˾¶n‘Jê0œŽ¤¸l&åfg«'Q*^ž` XYIZ*‰"!"ã®ïxä#ÿwYÞâ—J%  }Â:¹>.îYù3üX,L½½tôÃGW§ÓêR]>' _x¿ ­#Ù­Ž¯ÛiñK; ­P<|Ÿ#¥©óª2ˆ;š)¬uÙªu"º9ƒã}a{M ä©aufÀ(ã´BÄmvKóy­ˆjÌ-ˆãj=D “Àü'×Þ|¾”ê§«ñyž¸âÛßÚgÒ¦m¦]q¯/ú'ÿí“N‘R"èn¼Ö„óG³ÿ§7ÿú§ó-XP½â¦;6êìî:>ÃI³¶—Í,[§Ã6gØ2÷C·›*ÐÙ<¶õȇŽ6’›dȰGï‹x›S xÃGGB+i‘—5)ŸOk¹)œPwè…»¡këñ裎ãÀZjÌQg<¶zUºÄ Òî…Y3M)šý‰mŒrïÂÉTŠîþˆÆÿí­Ý:­æ…eo] ~6öÓÚ{ú·î;K15œV{îžæó†MÜ|lµ­ºT—ÏÈ_" OzJœ•ç–- ÷Àÿ—Sm¹ŸâU1–ÎnÇ9x¶“'njn–hÒgüH„ÃngEÑ€y¥n< £.)7_`¦gpÓÐ#vÖùмÓú4Qºvéµ7ÿ<èý{ñ·ûÖ×>é˜#>RĦ­EŒ9õüË6ëìêþ=ü~B ^½ ýOøû u¼¶„u7‡¿ëwsÁ/ýão¯¸*íØ/»ææ3ÁØøeo±\|ú1Ï$Ûo"(Ï™aTÐBÙ\̱Ž1õð6—mE`´T'O?Ÿ6å2Ât—ÔæÞKV¼$lСâa}æÈÜâzÆÈJ÷Ü~Ö<îéJ§îçÝÔ,GO0 ùÌt#ÝÉ“2± ¶wÒ=OÞ±¿N5†­Ãœò5þ^ÛÐù}Y÷O‡~eY¾ýMë}ã½¹AÝÀ>ì^~€ûEôÙòA¹~Û~ô?-®N©Õ¥º|ö‹¯Vã-%A^K›2Çœ¸äq¥/«àfkÓušUÖv@“–~ƒ-m÷ªñêõ^܈ª\ƒ;rf¤RkT¤¸ÆÈ°°éw2Ç1 t+æ‹‹ªÜpqTãWôâè1¿Ö G|„Û@‘™9Ó ØœtöÅ»õä ÷ÂÛM\ÏÖ¿'“ñoßt£ ž}Ú‰ÝÉ’»³rùz«::wþhõqôU­’êÇœ~þ–ûì9þ”öÙ#`N™—àb¡dAo×µ$o/&z;#g`Ö«Õ‘iê­Vyx)Â¥UZ?>->A€:·Í\G‡)Q}¶ç”üÙr7·$LÃÿÎ"yÕ1ÆØÕž«ðæxàVÇ…]p{{Æs7Ç ž Î ]ÞWå íF‚F4ÿÿ'|òµ¿žT·hå{ßÊï¡»Áû¾¼ï£ì½ùq÷Š&Ÿ{O" Ð^ìÚŒIu©.Õå³öÈÿ‘ðÈ“Í9Ò¢¤œÔÖÑ+G¥3OÛfT$À¶|D“iØÖBï;#¢²³=Æ5ËZÛ’RšÖšî¬›d§ãÆe#ïÚ'O<`ñ^«\¦Cê,Äíþ‹nÙ½”«€¼ìšsN¸ =û6ªŸôàÔ¯w÷þŽL¨ŽÇùݵ¹ìe7^y颞 êÖÛy=éÌ ‚à x»¹c8üáÆß\qRȼ†×M“ï°|ÅÊ…¬œ9oóÌØá»Œn¹[%@Å2<«Æ¦á˜ÑÔ9?Ú¶Läu-¨ãÛ™èa —•%Nn A'@騈¡q—±2¦‰ka׬BÙHê}nóèH4»×'ØùÓ£fœÇYáÚªjeߟy­7éõÇ'xÒǯݺ(K_ÉËâ çù¿á+µ.þ {Ù»d×3ö‰ú©N©Õ¥º|@þy%a–dhÝÎp‚Xë˜íA¤ŒæNØEŠ«”r Ä(‘Ùj¶Ø€ñ¶Ûk|³Ä’1lH‚áu·Ã’»}[ê4 CùT'ük;YYÀDïÊuÅ“ñæ‡ýä ¯Ý“ØqI€|vð {œìÄUXÚ…yäûžžÓïƒ%+ÚXÈpGORœyôÿíym†Gä0ôz­|ªT-}xÎE?Û°½£óðÎÃï?ëÚ__ñ[7ÊpÕu8$ò®”kÚÍf÷:g¡ŠÛíw?à?ÿÒ+ýµdkÆ/î×<´ÝJ«"@ÿ£Ÿl[,”v–J®[.ÂØß0 aÞU—³ÌÞ¾(¿Af·E"7Ésºð÷¼Yϼ¶c¡XÜÎ#ªØ„o×ä2ÿ8ô€æ‡÷‘0Œö$j ƒ;î~ÐáÅ×ì67ÝxƒŽSNQ9²ÿ²¥Ë†K)¿ Û÷ù™Ì /¾¼paõ‘þÏ–Áw>pqÏÊëá¼N$о¹tôÃ'y·îƒçvZí¥àèGvO½Ökîqnñ{»ª·moyö}}?\Ù=D*Õ®wP›ñß^zýwÞ¯žõêR]þC ¾ w{|g21¾l—‘W,¹S:æè­{,.sN*c¦ý(‚9ü²B\Ã;)ø‚ÛA ·Êmw!¸€…8Ëù}ýûÖzéIG,žÑbÖÃ×MS¿&¦"çg}ÐÞ7 Û¤2Ç› šÒЗ]ñë~/^ò0€ùHúª+—˺êŠK_rEW~sý-ß(Êcn@ë-Êf3ÇüìGÇ̶"+Çž~ÑÈžBñ~cðwvÙqû±§œpDá„Ó<¾X .¤}d÷²èï8 ÿO¯¹üÜ¥^¢.N¥X"~ëÝÅâі玤ú>|ºcŠÍ² ¶;;ã{¿úýu?k Y9^>,'œrñ.=ùÂá#Äů¾±èæ ÍÍ}Þ}÷Ý ”T‡Ãç%¶]„mσu¯|eÑë«>ÚŸlÉݶÿwJ*¸Ãùhå}oòvÇG˜¿ÔžëA¹~/+´ŸŸ]F€ÿüÓ¶YwÂí# %ùxÎÐ8@ØâŽÍõi}õv^úÀóâî9oÖî_[œvÞÞ…êU¨.Õemü>Z× ›¢19sÚe§¸“VRˆÝj´[Q à3IÕ ÉmZÞ4a$̘Þ*Â3N'² ãš%æP»‚ràu—~“«¶Û²¥ŠÊØX²K‚¥ŒF æºuNÆ8 †± ¯¼YgwÏ«Œø|¾'&^yæñÅpºÍóNzðéaùB±•ÁþºcúÆ$Å¥}Q>^IØ ûN“1PïG,m«ÎvÖ?ݤ½³³Í‚4j®Ÿ~òñl½ÕÊ#¹P|ýøÊ›ÿ`¼7â”_žóýä-7ƒå–ûžS(–¦Úý××Õ~µ»'¿'x³¿cñöïiË+uµ5ÞòÛK%¼dyöiçÿbÐÒå+ÿcùæÜw¬Š _{ã_Í›:%Ì4 oÂÚ_ÛfÛ…Ba¦‘øYÿ~ý¯[±bѰý᫽©Í¶Q}¼×~õ𙹹‹_ow²ŸùÜûnÖË<ÛUêy9|fDfxfkвô7ò9ä£Ým­ÚÝë.íÈc$éPº?ŸªËú'¯ºéˆ7܇¸Ÿßm ŸFæý_ßxÀ­Ï\²Oõ*T—ê²–@¾à>SGž¦‹ž&„aéÀ!ë›Ï›ª0ÎvôÅ-ÑC戊C›š¥«‡Û›Ö2Mc¸ï„Ç›h½JeDÚÀ zqk—£·ÍzÒ°„º,#›H Ì7yÑïnݺ;_X`½UÏGî»ËV÷¸½§ßã÷€7~ õ†7Ûpý¡ãGìÜe=|·‹—Û0Ä=¿¸/ÌS[iÑõÇã?úô  ‚Hx#—ÍŒºýºŸÎs‰[çüòæGÜö¤ãýÛ~÷>Èǹõþ8ƒAð$±ê-ˆ¿Ÿ-†Ï0”½e ¸/\oýõÛfÏÖ$ÀùOL)»g^|eÿ/{8¥éËËÄÔïß¡pÍ÷KðžOû¶äöȇÏrÆüüi€mŒ§>ĨP虯“܆ïûß|ùõׯ>âŸÈ+?¼òû£gÞè©{·îƒ×r3}í¸8·ÖË>ÔYêy…Vûçð¯ ÙaÖÞ× ¦«9îö}мš™FBË=ÁÏ»ëĦIGü¾õÀ@*”7Š!ø•õYÿ®>5þª%íùð<á=°/}Þ[WM\õE8'žñçu–tôì›óÀ0i[qãáÿªÞ5Õåó\м`h½Âz?j Q¢Ðw‰^¶¦?·â0Ëäf%W}É®Zc³ÿÎàÄõm9~5ÀØ´(£p:å²"b¿{)Fö×î€A´—œ07æÚ[§µdñ£'ÍY°LïŽk~€Ø|»ä4YŒ4È™4k"š–mj–,!`¸‡çOe3þ¸M7üõÛo¼bøŽ_Ûf§š\v²ì§yûÅ/¾ÄþcèžUò ~CÄß Ý{lcã.¯ýóà5®¡¡aGð”sË¥¼r·vÞ`u7)lû[â0¶–L&³Ç u×Ýáõ7ÿ¹¼vî׿ß`«Åkæƒàâ¿?ò(¯>âk¿ÜÙtöƒÞ³£;K5müç#ûx?à\“äú|„`Lõùwûr­m9úÎóÄ"ÿGCmf—¾5™¿vcË#¥@Ý‹ Ïç › ¬ß@üîÎBé¤Vt¿ ¿y˜ ã|mÖ{}ϯmØùIaÊü·ÿ£k?êgä܈ïc¿Ë}Ar{IÊ~Õ;¦º|Þ‹%_õ"j)‰Þ­Íÿ"öȨ!‡ÛUÌ »[O:ç„ÒCw€‘ѧ¼]G—ˆgÉXèC-G-ú,˜ntµ‡Ã:]²¼³V¥çi Ž Ã÷³¦›ºoÀmí´®³F€—Hó]O¸;ŸGï0z#»ìtÖ2#sêJãQàÚ=Œ„ñX/Þ5Bàß7Æ{ÁÉsÜ¥¿¼¶nˆz"®A¼é+Ê“  ½¤†a8ÿÓßôAûíØR«)=‚!MŒ@¾õ–CV¾¾ïLÚÇ}}ûíoúÇÂ…¯&·µíV_=¾ÿ¶óÑâšÚš}`Ý×Üõžy~Á{@€m£w~}¼N{GûIð÷¢ÕEŽ`¹}â!ß>îg—_c<Ìî9ôÎ'o÷Õ­?.•t˜—éj×sÏ9{ë½÷ùæ«ÕÇ|ív9¢õÊ«¯¼îãî»g…gOP8ƒÀý«[ôÜýnç”.n`^©>»jp=úÎÓ%+ýÌ>¾Öãß_ѵ;ßðïAðz?ë‹#vÞtàœùo-¹LJuº~,9›ñÄÁÝ8rÆÚŒ5sìdlÿ»WÂ^_:d½†C&ݬ¢!=vòq`4›æÜÀó |cî?äZÀ³:,eÝç:~ÿÝTï˜êòù¹pzXÓÓàS󄈕‡ 0ïl…`ð7yú7Æçëuf´LnÓ‹dbÛÖx[’zýs§ÝoAìŒÂî¶a‹&Sµ™<ûÐ1àµS¼¹cd8ÝÙˆFàÌñò9)Ááú»k–m/¿M’7öñæj4€¤e·ßxßã{9(óÔ÷¿ûí¢îMlp]APed8Xmp4 ¬‘£œð:cé:ß}êë§®lïèbFxoã·ß_¼ l÷9K8ûÛ\/¹²õÛ²rÕ›[n¾É‰î»G0lÏôR«-¶Üò¬¾ñÆõW¯Íçó¨&w®»Þ©'ŸìƒW}v"\~iÄÝ¥¾¾þgß¡ãaJªýŽýÞÑ—MºíÖb/÷é›Ûn·ÝIIÅð_íï`$<“úP;Çò$òUü“x¤ëo÷èô_D!˜-´®ä7¶iØøÔ—–¿½þ‰^èàw:>Îçþ!€ú¸õJÿ~«îà Ä¯$•¨Uê³û§=@| mvY}Ο€Ý1ïÍÅ­^ǒΫŽ»Õ¿ÿÞ¨`m½n0€·_K|µà§íÒÕ ˆŸ Þôo+†)ÿùûîøH"*´S¹u©{¬ÑÒÿ¤»6ë.–†­×PûÄ¿¯þöŠêV]>U R¼ÙX[Q xÖc'AÆÎ # b^Ü”¦Å]qëÉ Çå$=j™ðy×£›Á£$–,’Œµ’¡óIEMzF¥Íƒq[5s ŸÛܳ{œ‚GyuæŸà½vJó}ãSms˜Õáo…xÚŽcìþGì31˜÷÷)šà?¦Ùzx2Rzµ3–Næ;x¿=ÞŸô§¿"0îŒC/K;Âzϱ°«Wñ '½ÙŒct¼ÿèSS{ Yþý‰Ç»¶²ÕõÒ¿q×ü»ß^#N;ãôpó-ÓZ¶K„Ô?´î »{Ûösÿxá߸óáwMôÑvóæÍÃÒ½—zó’þöÐêIP`G2ò7¯>âŸl™¶÷/ ™Ûö»G*yË‘n:*¿pù¿^%iÖË èë[°”­ Vö¿š |2Y^÷ù’ì_ ä½´É®šŒ÷ ÐúÎ|¡þ½>¶Àó=:ËQS>ÉpûlÔQØ$`gƒ7®*€ø‰½€8Žç8O,½´Ñÿ<ž«]ÊŸóg{_Éwé){€ç¿?ü~|´bý†š‡ªwWuùÔ\ö–掲šÍo£7ÜDÿ¶Œö9Hn£ö¦Š<]Å£ðq#€‚ö,d7Nž¢6\®"àÏðkj–èÏ¢ü±M¸bèi—è·5Ž*X@,vÏÉ_{ ùÎE_Ù“öئÎy®†¼äžø÷¨}˽Ûa{; cMá|5 êÆ —¼û70'i"jcëÍÀíÿúæØï†&¼lÅ.JîÊ[“#—Ëý½»»™ß9òH¶¿uÒ¤MÈß¶ë … ÀfIq«Üça`ȽR±¸yo@¾¦cfåÊbuÕGü“/µ^ööÎR†¾±áÏß½üÐW˜I¿ g»K…­˜!23 ˆ,y´×uÿáÈiÞÑ·á5í£' à¾xUw¡$q<óC{n9òáO:¾öî"m );ïxâKa<ßñ<•übewq=xxË";°~[Ú†°œî§=7<§È¡©uæ™Gª¬üêòßråtžr<:Eao[j°è¯ë±‡ Îâ9t7²Ž^6né¬C›M¸Í!û®Ä+!XZ™S†Ömn2ám·ž7…¡ìçgµ„­«GØ6þV9ë—µÝ*— ­´ØãêîîñYT{-™ðVûpýF°sÁS/©Èxáú\;cqÁ±føÞÿ}õ±c?þŸ×ýîw8yíhçõ|>Œâ·pcÒKùêÖC†\¾Ú+62¶ì_}¿x˪ïÞ¿È¿ußÇÀ??õ]Ï* ã^nÆÛ2¿c—n6¨ÏÅ™c'#c}[ö«ïü~Ó”Cohyž@õ~øŸ€8.%)ÓJ1ð|è;ù)Õ6µ(—GtÝ|ä3i_þñØ1¯ƒ‡?rMÇõÛ'nó †üý„1·zWU—ÿ ¿ûÚËÊÖ3£·½þ6Ûs·¯·Réîºõ–g; .aåP†ÐÕo-1Ð-[[ðtï4©~æÖ[9W›g·œ.T*‹ƒCbIÎj­[éÑd™\E §íx¾gÏlR)omNúÚñù5îDõYÜB¿þÚkßÝÑÜA‰Õ6Hònpþvû»ËUÅ/æÀý8÷A8ƒ=2ÂÐ9,h€uFV0Gµ¾;–vôôÐ<ÖY¾Çö\ð›ZÏc”ˆçöžÂ¤£nú†·CÊg î_÷VÄÏ…ñT20_ì_—ýfo s•Âô•–îB°uÄÉxy¡zGU—ÿ ¿_ŒrÌn+.,îÚdÛí¹ p–<6ÓiuiÅaÂ25e{ƒÊÁ9Ò] ÿUhÓ}Žüç•·÷¶ŸuO½{ý•]…C7½¨Ïú]qãáokkö¸Û÷ðÞÎÖÂózhʦÚ_yåñÙc'sÚ÷_Ðý‡#[žü§ Wõ'òDðöñ€ÝGECë¸ÉûÁ¼ºeò·YO´vÞüݽý¶î„Ûw+j ?Ç|éçÿ×… }̤¶mòÅ`(¬°3×ÖHRDƒ^,ø×²žIÓ_ÿÎù?àXA^Äj Ãã&Æ»¦f^“ñk¿éˆWŒo±è0©Ôa°$sÿ˜Ûæß}Ró‘iQ‘úîØµÈ}±RÖ߈îÇüf FZ|!¿Ýà§¾$ÀÞ+<ùi ç${1Y÷ 'OÖzÞ®·nàÖDK"­Õûºš+܆[3íj¢WˆÚ¯ýÑ8¥sg“ØËH#ŠbÂð<Þí*™·9tÎSöÃãìpøñô°¥J¤þü¿qõ–/[Ž¡mÛålyCCÃgžK†¼“·KGb½óÕ7ý® _žåÞqç—ü[÷] ŽÀ¹.6©a†³Ñ7ФàÃïcñcË’Žñ³…IG¤P’çúû¥;Íïcຜôø‹ï=W{üígƒ‘ÑòeìŠ@^dŽ&g¢ÿ·G^x ‹tÖ-s\åˆÀ†=¯?xm¡ g}òÒ>çýÙܧ®â]ÑÔ§p´±^Òðw8ÕpÛöš“ñ¢6–ÿÇ÷£÷sr×7ÞÞë°2~æÑB¡pYà»î°ív;¾øÊËŸ:È–J¥ã \0ç™ùK>ÛjP·'Œ™Å£'jV]¾t \÷—”yXúÂ_o·FÛ7ֽDŽôƒ`GçwO.ï,ìJ Ï\ËG¿;ìSI I• ä…¾5™ùä‰Wñ_'uþZìgÇ”ßÙr½¾ºÿJ·ÂŸ?Ñä+ »~pÿº7ßü¸ýß)ÛÐÛï{ŠÁ ¬rEG}o¿}à¹wŽdåƒñ….G|¸ä{öâ)º{W¾ô8€rcšlí¿–t®oïgYÌ/µ÷± ôÖ)÷Þl÷ßÙc' ¢™S °šeýü÷êºùÈ9ÿk½Æ÷Ò nbcšðæ4=AW,Kd2 ànMyؤ„¼×œˆ“Çð¹?ÀûÐA\ÝûËK5xfЃOÿ´4JÅ?xå%|í·ç«óÎ+‚·âÿõÙô“Þq§žŸ?o7>èYuT‚úÖ§¹¶Û~{˜8ˆ&!‘æÍäÿÇ·çø õ°ïíÜÏ<Ï{/îýˆy˽SeÂÕåK°Æ_¥PŒÖ}b–àf:×ëW±€<ûVÀåÞLE@U’¦¡¹_ø#ŸÆxH~5 `ßXÙ]<@üâ ~€ø•k¹»4!˜—_øéÝø~YgÁpÐ'8Œ|ÿº¬ö\1§Þì0mœ˜ß¾RéǃNùÓ`8Σ*ƒªªhluîý}`þqÊqMoZkç°nÆÒË7» * géŒCÊ~X, c弚÷ò¥àWi N÷Ðs¡ótìä3Ò"Ö±gÊÒÆÕ'_”“7=óÞaÿúõ·Wþ/ve£Ðš6Ü}pѤ kLÀ›4Ã-h» 8OlÇG­Ë•ÄŠ;$´ÛG?{àWòÛlÏÝÓ¤±ÿaÿðÐL¶SŒåYîºçîÂ6C¶ú”ò6zpöÝv«¯üÊ¢×ïû4ö:á…|þ·ŽåùÆf›ož&&QLÜÌ«[Öˆœòî»ï6&<„÷ Ëeçr¹y]]]EkµÃ9»ó_ß|Á‹ÿx« _žÅÞÛ¥@gÑà1UuàAu›†ÃzR_Ÿq˜ :ûÁTàu”Tɶ— 2žø ”l‰b)ë‹yŸÆxNš<ë+iD7X6¯â+}Ÿ˜¿å¨{×f?ƒO¿g ìg›/q¾cœ´£—ú/Jí®Ê+7>„u&Ç1“½÷Á5‡.u>{;Œ+9,'¯Æc¯øÝ¿–t`o‚M¯èW› I¥À”–Â@çÂñ¶ Ξõ…x ®)ö4Å@®¯Q` dï Ï|ß?¸k“å7þNlâ‘j|š]ëW2 ‚úœ¯ ‹Üq“'¦8ŒÆÆoƒ{ë9_{!Ö+ê`X÷G,ÞjÈ+º€¿×ÿ¯‚u¯Ï(wŽÖÍ'‡­¯3ñ‚KRæðA -ÎU[³oÊDh=Ç¿‹€e¸+ƒÛÜïÛ—¨µ‘XÿD xØ›»a+x¨’7>Ûu·ÝþüÌüù?€ï´s7€ýú‹//üõ—Ï8ý´Ÿ9ݾÐþ *®Å ¥CCQ÷a_ç”SNÍ]wݵù^,õMF9`æœÙË{Eû 831yÍhi›c±?ÿÒ‹on½åÇ`›ûYË€e\O¬Âß—g©ñ²‹{‚ò!0´ž³Æ z—Tr q·ú؇ØNÊí0Á扄„ËÇëö­ù÷§1žÎ| ÉŸ}S¾J ·þ;—‡tÝ|äZ3ÆWvÐX˜üÜã<¬7ï¼ù»è1†^#xÖ8¶Ý`3µ8é¨ zÛ<_o«r5ªõ*T'8a®)3jyvé׋7þ£²cüÇn üûux5uÿáȶJö¼ :åOYÞYÀVÎ[º· •ནøMZÄ®Þ9OÂ=3öû/w©ÔÀõaßïÜ ‹›R@üæ‹ößé䟰“LŒk>\‡:ØÆb¨‰n® «ÿµgÔOîoH?xù%º¦™í/¼dµwøº\9x µõLa÷–3·ùø#í~T!÷û§r_k ßsϽ û=sòCày_²Ç{6<ùäç¹^ù×¶ÙöTýV  Ì÷ô<¸Ã¶Ûøâ+//ø¤eë!C.RQ¹>ä÷§“Üwßý¶êîî¾Ï}8PöôÕW_}ìà‰ßžxß”{+åÒ7Y²xñã››6½µ½Âþ/Æ-î©÷}ÿiëf2™+àø1|êÓŽßfÈV/¼úÆ¢{;Ưo¿ý¶ÅBqÇäqU—/ÞÒ?[¿jE¡Ã2Õ¹càvi ÃG®§Ÿ¿œ¬ë&bœ¨—üê¡íŸÆxJ2µ¼«Ò²ø“€8y¥iÀSï/µ—’¹`NÚ1¤Ÿ_Ý¾à ¦9©e­K;òG¸Ì£3áÏë°ïã¹²¡‚7~(+ÏÅ/Ü}û nq?Xº¾¨×}çÍé°¿-ãs½.M —toXgH%Û Àû´Â¤£nKûò°œzi§MÖùaÄ£yÉ.”äø±E% øàªs¢_t¼á y¾€ÛÖ£®€±.W«¹Ic·Û5Í5%¿K[ñÞ·Fˆ`gà ~Î`þ’¼Yù±*ƒ=äq¼©“ù!„È?ÿćàóÒ«¯<Àu|gµR7ÉçóÃg?0»sm.ư]v]oåÊ•WÁîÞ° :É]ïÒK/ó{zz&§Y¸ðÛÆË—#;ö»úßR¦yå{¿ÿÞ{Oo»ÕW¯îÓ·OËf›m¾ü£?¬YºtéN¥Réûðý!‰‰èÏ _{µ%mÌpüsø †Ç…ŽÑsxêc=Ï»µ¡¡aÁÆ›lÒÞÝÕ%>üè£þ];Âù=ØvL3räck$ëZ]>·e÷ vîºåõÇl…‚“´ÍÆ-÷ƒ‘±Ž¹bÁÃFm8ðƒç{ÕÚ «ôUšòñ?1wMLfwÙ%{ìd‰µ®¦iKbá®› Lí?þæâŽÍS@ó¼Ï®n_è…‰NGp,ƒ]î²ë7=óÞ~A$¶CÛ¿Æò·„G߀µØn6ðÛsÊÆçñ‹þíBùd'Æ.íèA9ß4.‚ý“¿å¨Ô”$2Ô1TŸ¼4ÔgU»µNÞ{pojýç´4`Ë59 Øÿ@ݷĵ.¸d­²ý¸ªÏFB/kRRfÉt* Œ]ÔV«÷£cÌó^\pÍVw¸ßXßäî­X ²ï;Â/l?»|Õqž¾s)d{ƼHìIfU$9Ö› `öàÉ^sÕÕWÏØ{ŸoV<¿£†XgÙÒ¥‡ÃoðuóM‹jkkš5wN¬×öœ9sv…‡vx/ÝþxÐà¿üåþRvŠd$²í €z×Ê+W¼ðüóKÉpœ²¹E ýÎìí:œ|Ê©—\íµÃ6tÆp‡,[¶l)¼–“Ç> ò[²xÉDøó‡*\~q—ß>5 ·7|dÐÛÊ .3VÜ€àÖÉ׃ñ çæ_(}à/yÞyôÿã-Ýëe j3§®ì*Ž£ñ… »üà®’9ÛÞ,¿‚ý¤Ý^švÞÞ…´ßä‹ÁxVÎ"_Ñ·&³Z¦÷Áƒ,²8K¼ÿÌE£q>ÿï¯èÆÔÕfÎ:¯ßñýÆG¿©uËdȼïzçü³WÞ_‰m[·LÏ4Ñ+kÜ/þž}öí¥›¹ œÇ”‰àùÍÒ3—{\âgˉèÆ×ÓftØÿÝ•@\[%ù]k ª5þFÖ‡¹ã&o ßWšW¿–r-> [Pÿ"ºÿŸÀ¿]ÇUATÄk¯ËМ•wR£Ûú©öx½ÆQ«÷ÌíÌÁx¾Ç£Ü½»$ZwoG@T $7:dC.-_àþ÷ŒwÿÚoü€{1€ñoa2ëCëP,8ýÔSœqúi3`â›+„x¾/Áz}á…VôXå ^pk}}ý÷žûÇ e“ðÕœ¡>ù|=ƒRÂ| ` 7Á6n$`ퟶ²ë>[SSs8õü®¸ œëV[mu ãë`Ìœë,%ÏHËk0–·Ó”Ä9_ÓR“Lâ>©J¿~J xXšHñqi {+Ĩóã…ÌrÁ¸m:R?˜–À£„žjý§1Ž'^zÃÙ{õ•e×þnæØÉ” ï¿öžâµl-ªJ®{ú¬u’²ŸY½xð»§Õk"€`¿¤3_Bƒ×Í‹÷ÏúY OÉ£'5F:È“l캮B©Þþ…màzü$Å€¸$m< 'ÞùUøý)XRÆ 1nmT—õ¯Ë¾Ÿ87»¤²œï]])Ôýá^—£¡0.åwa1¯|ê.›Ö“(ÑýŸÜ{8¦/*˜ûk›±åÞ.õ¥°ÆŸðþjVÙjñÖ㯿¿š:õé÷ÜóóÚ?ߘ4d³ÍŸ๠7åüfg®øÎ2Å)Iñü;¸à¿Ùu×Ý.ÿÓŸï)³ü÷Úëß^ÄeÞ÷ýJõº¹W½>eÇí¿ö|OOÏÊL>›9÷BŒéExîßjÈV7<üØß×èæÇˆ¼®Øék;ÜÛÝÓ},¶=%Eº†„mµs†ðúë†mø§§[Z:Êž@O,ƒïŸvì³××d `(-’\Nµ¦ž>Žêòéay´ `Ÿ+•\— )XWƒbÒƒ[\Ž@vvÀ´»“7‚.ö4Gå­u0´k˶>éÒS E^[~íøI}Öÿ-€ö÷÷fR×wû=·ùäšì§£çÿ±w%@Rg¸gf]¹/”KAb¢Då5X…Š5 R–ˆF4¥àAy¡¢Fã<Ë£^…(1Æ )Ä öäQŒóÖáújøû‚ȽâÔ$Ì¢v]œ—“$þ1DúÈI}‘}ùjcN6:æH"tø™½}:&µ8¾ûƒky¿´1+{yý³Ïu,8RkÆ%œq}¾¯§N½%'Þ>«×®AA‹Î;gtëM›7÷uB!hÍ¡ÌÌÌmõI[µæÇ_éO]„î{°Ðk´‡ü@Iûöí·š®?¬]‹üÚ³S½6 (H/zG ¸VË5•`éXç¨p™ÖçøŸ§n —ÔÑ´ˆõ‘8œúâr޹èžSP~{ÚaažXÔffê…HH+Ž4ט(q"ÅLJÿíãaÉÔï&lÙ¼­Ïaí~±í¿<§°ÍRìŠg(Âòê’Ÿ·Òsìõ{ÈBEGß6¿½ii mü)×ÌO×)6¦í@Èqê,c]§ÌíJÇN6iÃQ¹äHÛ¾«òC^;]÷îGÄ?@yIW11C[7Íúëô 4"dô ›³8<çªx.yC.Eœßàâ™z†å†dWDÊˈ ¸•h9Nt2•]IûO§ã¢–d$BŸGæ¡£`J¹þ:"<à›Fª¤üïé˜|ó=й!EÃG=Ù¢!Ø´ûì@ 0yĈNDvÛª¼|þ¬q¯mŒg¹|ÕÊ­<ð-ïf/õ±vVàöAêOYšT¤†wlï{¬$îLör"ž±¤•¿OÄ1ˆºÝ9~ßT"|ôŃI›P_"÷tÛ||ÏÎ{„Ï®í[=•W²{¢2ÜU„–e\Ïã6åëÀä¶ýk‚¡ã-š"¹RÉ^‰¾iôÖ¥”m)ª€6®Òesx‡·5!¦Ð‰ãD\fÅ•5ÑIÙ§|›µØs‹*P¸å¾à.A ¢ãwùÖj$ý†…ÈÍß ëÆa–ó~àyØR ‹YXwR$Oìûy£ÊºMHê:q{ù#D XœÁ[Xy ä¿Ðש¡,ë0mwLNÒþÒß!„þ#›×žÃÿûö>Òä fmÞäYœeäÈ3}<-¼¯9oPC^%ê,W‘*W¤¬„Q¼áÉ… ?ÝãCaCóW*6ðÈ—ä@€xjíû™LàyD‡2”:ÊFD¾¡ðƧ·u¹~î]%»CD\ˆß§ Ô"¡ŸYkú@"yXYÞK÷>…¶TD´º®eo}z\iåOØ ˆÐ¶iÝnœûf¼R±X¼Ã~oRvÂÖ%UwtëÐúçdŸ(¨£ kæS "žIBE…¦¹—Ðw˜2ÝSTCѶb £DtÏÐç~ÚÖÛzšˆÛêÊ£c@´gY¾2Ý ¿÷ äoF,ÛŠè~š{ŠöN#Ä’9ˆùCs;6|lW3¡÷оBÚË(ú’ëCt¼ÕCä0̓Î þ~jgØ”hð,^ü™+0,i`i2Ý}’í¬"´0d—åAïHäøX•×roí ûëò‹^Ÿ›yõë÷’4ù,‘÷8ÒÊgbéRú|kè¡¡ÿîÎ÷Ú®ôâŠtîcÕæBLi!À˜<í{už¹rSü¼fÕ!…åÕ°¸ÝìuùËrWV3¹÷"$ô\޶YÙ3Æ–&;ÆèˆÆŽBá6D˜£Ttà]ñú9‰´ñL+Uô’À0KOT±ùè3ˆ ÍØ”-ðvž‡¼€´°nÆ6XøL÷Ã˱XQ¯0Á#)´awz=éþs[™«}Eæ~™¦R&øZj($~}R BéÄDÆ7‘VÝ:•sÓ1}¨ÍfÐÆdµÖï÷_B×>_#ñæÖq“iN -œF40*ƒÕêHÛçCìF"ô•NØ©+‘šá;hOêRͬ Ïy½E\4šH©Ê{Òçs}‘øT?땵³ìÒtï£6f›³ZÉTÈ1>(à¿ÇN˜j –õê¿¥UµÇ¡ IXEý«¥ß½Ú«/“FÜ $¬7ÚVœJ_¦ýméq§P{ÌØö*™™IR¡ŒÕY¹0µi³yÉr–mè:%Úx[VfL䨈åR‡m›UtîDãç2ýÚ­Y‘jihÔùJˆ<}Bϧv+´_Qg’ ‡¿!b¾P#êó©=KíMj·S;Š··£6'»‰ú{¡É³€|ZMuõ‰Ÿ}¶(®Y²oï#Ûö;ªÏÉì—ov ûòS@m(µƒëÑùJPhÁa¿2jœ`÷ˆ)Å·Ë™"-‘þžF›òúu蕎5 w—k‰À?âò˜¨V¶“êÒÊÿ ÓoÐq¦u2·M:Ï´ú!¹âßnºÞ¶w_õÊ„D¨ M‚EÃBDF™¶w 9CêVQŒn›è˜ì8ïÓÖŸeó³N€8hâaQ}<ÃeñœMÞ²ùe*6…n:¨-sÀf)ìG÷ÛÅBâØ†bWÇZŽYKçjû"ÐͶ JBe„ÎSªì~íétÞ«šþ&Î>†Új‡6ãáeëOáÚ}ìoõЦ>PA„šî¡£Ù e«õ®Šøµ/·˜®°¢ÙÔbÊN¿ ÜUX¸¹¼¬¬§ã8Ci%MLJjQōܯ¹Vh° ô>¾BÔ¼n~Gí°&z¼¦Q /ðû7›ÅU6æÈ:/u¬sm›h…º7QCÃÛYKDÄk•?DBóõò³Y ·â‰ë¯d KH"XæXNº+ ¥±éo]Ï}ÝËec©Çw¨˜8Ó²}>ß»,X/ý#&]Kù9eòX„ÅíKs Êü'e/Ìäi÷z|‡Üê“â EaˆœªìÁgKøš˜sÚñ~è}<®‹ä Úÿ‡x6‚ê¾Kâ½›ó—Ž÷©½Áš;™ ž[`-ºHEŒ!õr…öËq+>ò†ÖÓý¢nb‹LRºïh¬Ç1°6óß7Dâ÷UWU-.ÈÏoEgú?Ó6ý}Áô’Ïï¿ß·w {HpúÿCMøŒÂL~½âì÷(µ‡àçÞ]Y© vì@*J{ú?–%„çVË€3“'Tùí;—ä.eí Ç 6WÙ¯€ÉK#Íš>‡EF?™ÀÇÎáë]ÀRú2ÞÓ[ožzòož£¢—gÌàë`‡oK_ßA1ãÕÞt! ·5É}k„®¶„ûq ·°@ƒó‡ g-|dŸ²°ƒûÍU±A‰ØÿJþÏHwµÂ-‰Ú‰­(Gñ³Œ.uZg^§®ÓŠ8£º ʽ™çiþan°¦ÀÊt1[Xðï3úƒ‰Emýzy’¿KafY”s 'Œ“ë}ÊTl$¶×x‡}÷Õü슘Ä_Oâÿðòù&:v™ù&»` bv–ðx‚+¥›v}lYí$Çg}Á•\c~‘?®bÓÚ¾M³/~Àm4?_¸|ºò3òÜ…yg=+<ÿQ ´XOs‡»¨G°[< ¸çNï@Ôˆ´×;¡Úž›«jkk¸æé¬T±¹°.°4 |òy÷ÃdòwèsU´okP](¸„oGž°ÜA‡E&nâI®-RD#Àd ŸÃ=ï›<‰|¤ö®YíZ&F2™*>¤Û……·Ê× L²ˆ°ÝÍ&2˜Ê6±0¿Û-ZjÚÁçêÍB‘ë—½—ï-‹z_C°À½¡jÕ®9Ù¥÷[ƒÝz±ué2~÷ãY»‹]ÚøjRÒ zDLí‰q¿¿?ñç·¸ìÜiF ·¹‚ÓÌŸ±“Ì=J°› >€TzªŠzqÇTHÐÆ+ÊËUMuµòGÖ\uKÆ[9é69™@Z"Tû³©n“õ8Örßçýú3ÑYÃþ‚Éð ž¼?ç}AæÅ¬•!i0“,ˆpkÙcXB˜Iøq¾Þ£¬+>Ü´A‡û<“ø=ü9ÀÄ}7›Æúó¹ñæØLð¨ å*•ð¤ÁæÖ^þÇÍ6ñÁ:1[{NnÝú¥‹zb DùÝC0|Å>åÓ+íH¼³g²çÞÎäýkþ\šÎÛ' 4ô–‚F%ñ}@LÎþŒ[’´%h\³9è6ž¤à#šËÚÈ P_¡——–*Ÿßo¾›Ú4®·Éu_{9 GÖ‚(á½ ²œqM‹µ}ÝUg±¦ñ*>/|ö¯i×DMgªØÌdZÃdHœ{…ppŸï!62©ãüÛxŸrÖðÝ<Û<þNÏ»õ±ò8kvn ‹»ÞùmßVüN”ªGÙÐRœÄ}à¯*âsµ° =þŽì'O¹ÕzúЇ>ö¼†@ ¼ @”Ü0!î1oƒ¼AâUUUÈ7K§´_•‡ö¸Ë$¢€ÅpíÞôÜrw±‰ãù^Ý ÂÁâ'þkúæò¹õd"ö*Ç­ =Ýú kÌ®ÉÝ–¶çóè»îv[mëOYcÉ÷“Ë¿Ÿ‘6´Bº'sñÕŸÄûú¢}teò&ÑÈ›;¢×ÇQ¥»vy­P–Î;òY€¦YðW»é<Ъ¿Óš†q·úU¢û1MpaÖ–}Êž¢4‹Ã5,@ ‡IA){êJ"ØÖ*F4í<^Î2´óùJJ» ÑÈiKR¬WÇjã®°SœÃA´ŠËQñÓ^t5lr-h™…ˆ+øûŒ$Îë‹ó]" B‘Ëð{#â±0÷#Zîˆ'àQÿ~-kùˆ8“w¥ !rAÚ@Ö‚Ü,XÃDŠ ø˜m¦iøx‘Ò‚ [7¤xéJ&o ð×o‰³of#>œ»ÿÖ¹Ü2´ß†¢"0¯«çuð?,òïa"FÄ?Ššl’^(Z´B( éSz°¶Ö¦ˆèE¢…½*.MÐH*U@]Ê}àŠ&| ÐŒáמ£mC*‚ïÜZÌÝ-}¶:ÅëÀ²0…¢ûyÛ,é…@ˆ\P&¯Ë…<‹=MgBGdDt÷e"ÂÚê™Lâ¤y°fþµ¿0aváó×UšÛOáÖÈe#ßRïï~†Šä•oÐöw+G!á¹_ŠÖ‚×ù|Ç©H”üBé„@ˆ\>«HÚ™ò.ʳ/ÒÅàCF]u‚!åjkÑÐZÇ29éï³å\®E_#æjø¥‘«Ž\ê­,8,cᡟv«mùüfŸqƒËlË8¶µô³¶|/î¶\þ H'{†ï¿ñMÖ¢±ö°nöwW«Bê"ñ‡¿9^`¢áógX; ¥ ‚–ñ‘7 ²Û)¬}£,!Š©d3!ý×BT xäb›ÒÁW*bŠ7KM"g‹˜ ŒéPÖŽ!0 ÷Ü­/ 3ö$&Ë<ãøLˆf w˜Ç±Ld;µw”íZdFl3'BCI6¸.6?;2Lac_1hÿúÿþ£9ÿ| )€/*-+CBÿ| 1Qdbs8mk%K‰›q&fÀÉÿúÿþþ·- “ÿé~:/H¤üúX Nd–…#Ùÿ?Qÿ‘ q7 kG'ýÎÁÿ,_ÿ®BÿׂÿE"ÿÃeÿí3 ÿÉ/ÿˆEÿŽzäÔzØÊÙýªKÿˆ|Ù|¸[Èí%ÿ¥*ÿ²mç6Äìf|êUÿ|Òÿ eÿ—ÿ•ì¥ÿ4nÿ’-“:YxsaH ÂÿˆT@ÿà+Íÿâ~OIÏÿˆ‰ÿ×–…îÿþÿøÿnºÿØNrVŠ5uº–il32 ÛäPGHHGHIJIHGGRî…ÿØ‘ï…ÿÙ ˆ FƒJeúÙ …€„øÙ ‚€&øÙ €‡&øÙ Sˆ€ &øÙ pÕ¬T €&øÚ(ÄÁÈÇ( ƒ €&øÙ…Ë¿ËX %$&ƒ&$% €&øÒ+¼ÂÇŸ $#$!"$#$ 'øÌeÉ¿ÊJ $##%%##$ )øÒ”ÆÃ´ $##%%##$ € *øà°ÂÈ€ $$#"###$ € +øï¾ÀËs€ $%&&%$ € +ù÷ÿËe€ $%''%$ € +úõÁÀËj€ $%$#$$%$ € +ùè¸ÁÊ€ $%''%$ €^+øØ¤ÄÅ£ $%&&%$ *øÍ}ÉÁÃ) $%#$$"%$ (øÎEÆÀËv $%%%%$ €K'ùÕ ÆÂ¿$ &''' '& (ÿõµ«Â¿È™  jÿÿ‚’ȿ˃‹!!€ &ÿÿn"Ãÿˋƒ@5"!/ÿÿxPÊ¿ʮN ‚16Æ«zC!!=ÿÿxYÇžÄʨm=!*¤ÊÊΠ!!Gÿÿx€AµÌÁ¿ÅËÈ¿¹ºÂÀ¿¿Ë~! Aÿÿx~ÁÌÅÀÀÁÂÂÀÀÂÁÊC >ÿÿxƒ'q©ÃÊËÈ·Á¸ :ÿÿw… &F\dbS9l–€ 6ÿÿ}… ƒ "M¯À¿¿ÀÁÃÄÍäPGMXFFBFR`kpplaSFBPî…ÿØ<4CHJIC6ë…ÿÙ U?ETSPPQRRQPPRSNjMGJeúÙ $TRQORTPIDBGNSSPPI5øÙ 1T€OC( :QSOSL€&øÙ BWSSH†?TORN&øÙ -:EP,†/SORH&øÙ $  ˆ,TOT4&øÙ ! !!ƒ :SOR #øÙ " " ! "ƒ" ! MPT1#ùØ !   .TPF1ö× " ! ! PPMFôØ#! !! DQMXôÛ'"€  € 8SKc÷Ý)"€ !""! € 0TKhúß*"€ !""! € 0TJjüÞ*"€ !  ! € 7SKgùÜ)"€ !" "! CRL`öÙ%! !""! OPMRô× ! ! ! +TOL>õ× " !!!! ORQ@*ø×" ? "# #" #" +KY$#ÿõ¸c "  ]s* jÿÿ " "‹X¤¤p€ &ÿÿv!! "„ ?ªš™¤p+ÿÿx " " ‚1 ! t¤™™¤pqÿÿx!! !" ""# w£™™£ms¤ÿÿx€ " !"! !€ " w£™™ŸŸˆÿÿx "!„ ! " pŸ€™wÿÿxƒ!‚"!  i¤œ›˜dÿÿw†   € 9qvƒŽTÿÿ}† ƒ ¯À¼¹¾ÆÑÛÓäPGReED>DZs‡‘‘ˆu\F>Nï…ÿØr3c~‡Š‹‹Šˆf5ç…ÿÙ (˜q{–“Ž‘’‘“•‰ˆOEJeúÙ @–’‘’–ƒyw‹•”ŽŒh øÙ X–ŽxG 7h“•ˆ&€&øÙ uœ”•€ †$q–Ž’‹ &øÙ 0QgzN†T•Ž“€&øÙ .% "ˆO–—]&øÙ *)++ ƒ h•Ž’ ùÙ +)+ "!#ƒ#!" Š•]!øØ&*+" ! ! ! R–ƒ9ôÖ))+ ! " " ! Ž^ðØ)(*' ! "" ! y‘Ž~ñÛ/'+€ !    ! € d”Œ’öÞ2'+€ !"##"! € V–‹œûà3',€ !"##"! € U–ŠŸýß3',€ !"! !!"! € b•‹™ùÝ1'+€ !"# #"! x’ŒôÙ,(+# !"##"! Žsð×$))* !" !!"! M–ŽPñ×(), !""""! “‘y.÷×+*)? #$$# $# O‡ŸEÿõ¸g!)+!  +5K jÿÿ+),Œ)MM4 &ÿÿv**),„ PHHM5.ÿÿx+*)+%‚1 *% 6MHHM5Oÿÿx+*)*+$  #++, 8MHHM33gÿÿx€ ',))*++)((*€) +8MHHKHYÿÿx*,*€)**))*)+5KHHEQÿÿxƒ$*+,+')(1MIHCHÿÿw†  € 47=B?@ÿÿ}…    ¯À¾½¿ÃÈÌÏl8mk,Gj}~mK(Ð b·êÿÿÿÿÿÿí½kCÿÄØÿÿÿÿÿÿÿÿÿÿÿÿæumÿÿÿÿÿÿÿèÖÒàøÿÿÿÿÿÅ)™ÿÿÿüÐx3 #[²þÿÿÿé<Îÿÿÿà 9Áÿÿÿï3 bG‡²ÖóƒÿÿÿÝŒÿÛ[-†ÿÿÿ 1üÿÿÿ-D@> GFG²ÿÿÿ8­ÿÿÿmLÿÿÿ«´ÿÿÿAðÿÿ­#ùÿÿÌJÿÿÿðôÿÿÿ?ÿÿöoÿÿÿZJÿÿÿÿ]gÿÿÿÿ?.üÿÿ_°ÿÿìJÿÿÿÿ´¾ÿÿÿÿ?Øÿÿ˜×ÿÿºJÿÿÖýô"ùûÙÿÿ?®ÿÿ½ëÿÿ“JÿÿÒÿgsÿÊ—ÿÿ?—ÿÿÏ!ñÿÿƒJÿÿx€ÿ¾Éÿvƒÿÿ?–ÿÿÓïÿÿ‰Jÿÿ}'ÿù$ýýŠÿÿ?«ÿÿÊâÿÿ¦Jÿÿ†ÒÿaoÿÈ“ÿÿ?Óÿÿ²ÅÿÿÖJÿÿˆzÿÉÏÿn”ÿÿ?(úÿÿ…ÿÿý1Jÿÿˆ%úÿÿõ”ÿÿ?ƒÿÿÿEEÿÿÿ—Jÿÿˆ¿ÿÿ³”ÿÿ?øÿÿã Úÿÿö)NÿÿeÿÿXÿÿ@‚èÿƒiÿÿÿÄ+  U™\ žˆµwÑÿÿÿ¡‡ÿÿ±6ùÿÿÿ­I=_ÿÿÿÿ¯bÿÿÿÿÞ_AûÙ™Q*µÿÿÿÿ¬ _kÿÿÿÿÿ؉J(0ÔÿÿÿÇ ¶ÿÿÿÿ«ÁºOåÿÿÿÿÿÿüòõÿÿÿÿÿ¡ ºÿÿÿÿÿŒ›ùÿÿÿÿÿÿÿÿÿÿÿÿQ ²ÿÿÿÿo-ŒØþÿÿÿÿÿÿðûò¨ÿÿÿÿR.WuƒjF‰ÃU²ÂØêö7P) it32ghþÿýÿðž “Ÿ “Ÿ€ ’Ÿ€ ˆŸ¢—ÛÿüÿþÿûÿÙÏ ŸÿöÿþÿûÿÚ ’   Œ   ƒ  £ÿ÷ÿþÿûÿÙ“š… ŸÿöÿþÿûÿÙ‘‰œ€ Ÿÿõÿý˜þ‚ÿûÿÙ’…¦  ŸÿöÿþÿûÿÙ’‚‚¬šÿîúõ—ö÷‚ÿûÿÙ’‚€® ŸÿöÿþÿûÿÙ’ƒ± @h\a`_“`_cZxÿþ€ÿûÿÙ’½—&ÿý€ÿûÿÙ‘½  ‘   .ÿý€ÿûÿÙ‘¾’&ÿý€ÿûÿÙ‘¾‚'ÿý€ÿûÿÙ‘Æ&ÿý€ÿûÿÙ‘žœŽ&ÿý€ÿûÿÙ‘˜‡—Œ&ÿý€ÿûÿÙ–“–‹&ÿý€ÿûÿÙ“™”Š&ÿý€ÿûÿÙ‘Ÿ“‰&ÿý€ÿûÿÙ£’ˆ&ÿý€ÿûÿÙŽ§‘‡&ÿý€ÿûÿÙ‰ƒŽ©‘†&ÿý€ÿûÿÙ‡€©…&ÿý€ÿûÿÙ‡ ª…&ÿý€ÿûÿÙ… «Ž„&ÿý€ÿûÿÙƒ OÇj€Š¬Žƒ&ÿý€ÿûÿÙ‚(Á¿Îºk‚…¬Ž‚&ÿý€ÿûÿÙ„ ¥Å»»¾Ð­R †€¬‚&ÿý€ÿûÿÙyιÃÁ¿¸ÃϪ=­&ÿý€ÿûÿÙ€<ɺ¾¾¿Â¾¸Ä˘;À€&ÿý€ÿûÿ٭ĽÀ€¿¾À½ºÈ͇%ÃŒ€&ÿý€ÿûÿÙoϸ¾¿ ¾¾ÀÁ¼¹ÌÄ‚ ¿ &ÿý€ÿ ûÿÙ*üÀ…¿¾¾Àų˖ÁŒ &ÿý€ÿûÿÙ„̺Á¾…¿¾ÀºÊ ÄŒ&ÿý€ÿ ûÿÙ<Ç»Á¾ˆ¿ À·Š” ‡ ƒ‹&ÿý€ÿ ûÿٚɻÁ¾…¿¾Á»È:€%#†$%#'•#ˆ$%#' ƒŒ&ÿý€ÿ ûÿÙAʺÁ¾…¿¾Â¹Ïi#!‡"!$“%!#†"#!% „‹&ÿý€ÿ ûÿي̺Á¾„¿¾À¼Æ¡„$"Š#’%"‡#$"& …‹&ÿý€ÿ ûÿÙ+¼À‡¿À¼Ç1‚$"‡#$"&‘Š#$"& …‹&ÿý€ÿ ûÿÙpι¾„¿¾Â¹Ìwƒ$"ˆ#"%‘&"$‡#$"& †‹&ÿý€ÿ ûÿÙ·À¾ˆ¿À¹…$"Š#$’%"ˆ#$"& †Š&ÿý€ÿ ûÿÙK͹¾„¿¾Â¸În„$"ˆ#$"& %"‰#$"& ‡‰&ÿý€ÿ ûÿÙ“ɺÁ¾†¿À¹†$"‰#"$&"$ˆ#$"& ‡‰&ÿý€ÿûÿ× À¾À…¿¾Â¹În…$"Š#"$$"‰#$"& ‡Š'ÿý€ÿ ûÿÖPι¾‡¿»‡$"‰#$"&$"Š#$"& ˆ‰'ÿý€ÿ ûÿֆ˺Á¾ƒ¿¾Â¹Ìƒ†$"Š#"$&"$‰#$"& ˆˆ &ÿý€ÿûÿÚ·À†¿¾ÁºÉ9†$"‹#"% $‹#$"& ˆˆ &ÿý€ÿüÿÞ=ɺÁ¾ƒ¿¾À¼Æ£‰$"Š#$"&‹ %"‹#$"& ‰‡ &ÿý€ÿüÿßk͸¾ƒ¿¾Â¸Îh‡$"Œ#$ Œ&"‹#$"& ‰‡ &ÿý€ÿûÿ݌ʺ¾ƒ¿¾ÁºÊ:‡$"Œ#"% ‹ $Œ#$"& ‰‡ &ÿý€ÿúÿÖ ²Á¾À„¿À¾Â° ‰$"‹#$"&‰ %"Œ#$"& ‰‡ 'ÿý€ÿùÿÑļÀ„¿¾ÁºË‹ˆ$"…#"„#$ Š%"Œ#$"& І 'ÿý€ÿøÿË?͸¾ƒ¿¾Â¸Î]ˆ$"…#"‚#$"&‰"ƒ#"$!$ƒ#$"& І (ÿý€ÿøÿÊ[з¾ƒ¿¾ÁºÊ:ˆ$"ƒ#"%%"#"%‡&"ƒ#$!$ƒ#$"& І )ÿý€ÿøÿÊyϸ¾†¿ºŠ$"‚#$!' '!$‚#"ˆ%"#"%…#$"& І *ÿý€ÿùÿυ͹Á¾ƒ¿À¼Å¦‹$"‚#$!& %"#$"&‡"‚#$!'$"ƒ#$"& І *ÿý€ÿúÿ٥ǻÀƒ¿¾Á»É‘‹$"‚#$"& $‚#"%…&"$#"$$"ƒ#$"& І *ÿý€ÿúÿןȻÁƒ¿¾Â¹Îy‰$"‚#$"%%"$‚#"†%"#"%…#$"& І +ÿý€ÿüÿê´Ã½Àƒ¿¾Â¹Îq‰$"‚#$"%ƒ#$"&ƒƒ#$"&…#$"& І +ÿý€ÿþÿøÁ†¿¾Â¹ÎH‰$"‚#$"& %"#"%ƒ&"$‚#"…#$"& І +ÿý€ÿþÿô¾À…¿¾Â¹ÎH‰$"‚#$"& &"„#‚%"#"%…#$"& І +ÿý€ÿþÿö‡¿¾Â¹ÎH‰$"‚#$"& "‚#$"&ƒ#$"&…#$"& І +ÿý€ÿþÿõ¾†¿¾Â¹ÎH‰$"‚# $"& %"#"%&"$#$ …#$"& І +ÿý€ÿþÿõ‡¿¾Â¹ÎI‰$"‚# $"&  %"„#‚$"€#$"&…#$"& І +ÿý€ÿþÿõ¾À…¿¾Â¹ÎH‰$"‚# $"&  $# $"&$"‚#"% …#$"& І *ÿý€ÿþÿö‡¿¾Â¹Îf‰$"‚# $"& &"$€#"$&"$€#"$€…#$"& І *ÿý€ÿûÿܤǻÀƒ¿¾Â¹Î{‰$"‚# $"& $"‚#"$€$"€#$"&…#$"& І *ÿý€ÿúÿأȻÁƒ¿¾Â¹Ì}‰$"‚#$"& €$"€# $"&$"‚#"% …#$"& І )ÿý€ÿúÿ֚ɺÁƒ¿¾À¼Æ£‹$"‚# $"& &"$€# "$&"$€#"%€…#$"& ‰‡ (ÿý€ÿøÿÉxз¾ƒ¿À¾Á´Š$"‚# $"& „#"$ $# $"&…#$"& ‰‡ 'ÿý€ÿøÿÊnи¾„¿À¼Å&ˆ$"‚# $"& %"€#$"& %"„#…#$"& ‰‡ 'ÿý€ÿøÿÉIз¾ƒ¿¾Â¹ÍOˆ$"‚# $"& &"‚#$ &"$€# "%…#$"& ‰‡ &ÿý€ÿùÿÎ.ɺÁ„¿¾Â¸Îxˆ$"‚#$"& "ƒ#"% !‚# $"&…#$"& ˆˆ &ÿý€ÿúÿÔ¸¿¾Àƒ¿¾À¼Æ Š$"‚#$"& €%"€#$!' '!$‚#!…#$"& ˆˆ &ÿý€ÿûÿÚŸÆ»Á¾„¿À½Ä%‡$"‚#$"& € %"#"%%"#"&€…#$"& ˆˆ &ÿý€ÿüÿßt͹¾ƒ¿¾Â¹ÍZ‡$"‚#$"& ‚ $ƒ#$$ƒ#"% €…#$"& ‡‰ &ÿý€ÿüÿßD̹¾ƒ¿¾Á»Ê–‰$"‚#$"& &"$‰#$‚…#$"& ‡Š'ÿý€ÿûÿÛ»ˆ¿À½¿!ˆ$"‚#$"& $"ˆ#$"&…#$"& †‹'ÿý€ÿ ûÿ×ǼÁ¾ƒ¿¾Â¸Îf†$"‚#$"& „$"ˆ#"% …#$"& †Š&ÿý€ÿ ûÿÕgϸ¾„¿À½Äª ‰$"‚#$"& ‚&"$†#"%„…#$"& †Š&ÿý€ÿûÿÖ-ƼÁ…¿¾Â¹ÏL…$"‚#$"& ‚$‡#$"&‚…#$"& …‹&ÿý€ÿ ûÿأƼÀ¾ƒ¿¾Á»Ç—‡$"‚#$"& ƒ%"ˆ#‚…#$"& „&ÿý€ÿ ûÿÙgϸ¾„¿¾ÁºÌ@„$"‚#$"& ƒ&"$„#"%ƒ…#$"& „Œ&ÿý€ÿ ûÿÙ!À½À…¿¾Á¼Æ †$"‚#$"& …"…#$"&ƒ…#$"& ƒ‹&ÿý€ÿ ûÿٌ͹¾„¿¾Â¸ÐVƒ$"‚#$"& „%"…#!……#$"& Š&ÿü€ÿ ûÿÙ=źÁ¾†¿¾À´„$"‚#$"& „&"$‚#"&„…#$"& €€ˆ&ÿý€ÿ ûÿÚ  ¬Ç»Á¾„¿¾Â¹Îx‚$"‚#$"& † $ƒ#"% „…# $"& €…%÷ôöüÿûÿÙIʺÁ¾…¿¾ÁºÊ;#!‚"#!% …%!#"#†…" #!%  "‚ƒ&ÿý€ÿ ýÿðž ž ¥Ã¾‰¿¾Áµ‚%#‚$%#' … &#€$%#'……$%#' !!ƒ€^eTÄÿýÿò|ŽÀ†¿¾ÁºÊ“€„ ‡‡†  !!„ƒ Ÿ€ÿþ€ö÷ô÷. È»Á†¿¾Â¹Ìqº !€!£…ÿúÿ7и¾†¿¾Á»ËL¸ !‚!Œ Ÿÿ€þ ÿüþ*—ƼÁ¾†¿¾Á»ËK¶ !„!‹  …ÿþÿ*-½À¾ˆ¿Á»ÌJŸ !†!Š Ÿ…ÿ ýÿ$gθ¾†¿ ¾¿ÁºÌJ œ‹ !ˆ!‰ Ÿ…ÿ ýÿ&¤Ç¼À¾†¿ ¾¿ÁºÌK›‡ !Š!ˆ Ÿ…ÿ ýÿ'*¿½Á¾‡¿ ¾¿À¼Ée– ƒ !Œ!‡ Ÿ…ÿ ýÿ&[зý‡¿ ¾¿Á»Ï‡ –“‘T€# !† Ÿ…ÿ ýÿ&~Ϲý‰¿ Á¸Ë®-’=ÔÌÀh7 ‚"Ž!… Ÿ…ÿ ýÿ& ŸÇ»Á¾ˆ¿ ¾ÂºÃÆfĸÊÍˬ}I"Ž!„  …ÿýÿ&»Â¾À¾ˆ¿ ¾Â½»Ï¢5€Ž,ɼ½¹ºÃÎ̾”Z+€!Ž!ƒ«…ÿýÿ&%¼¾¿À¾ˆ¿ ¾ÀÁ¸ÇɃ&€‹wͼÄÁ½¹¹¾ÈÐŧx8" !ª…ÿýÿ&CͺÁ¿¾ˆ¿ ¾¿Â¼ºÌă&‚‡¸½¾¾ÀÂÂÀ»¸¼ÅÍ˵‰W!!Ž !) ¤…ÿýÿ&J̺ÁÀ¾‰¿¾ÁÁº¼ÍÈ?ŠpѸ½¾¾¿ÁÂÁ¼¹ºÀÌÎáj!Ž ! !& ¤…ÿýÿ&JÌ»¿Á½‰¿ ¾¾ÂÀº¼Ìʯm)€€ §Ä½À€¿¾¾¿ÁÂÁ¿º¸¾Á×4!Ž " ) …ÿýÿ&€ J˾¼Â½Š¿¾¿ÂÀººÂÏŧw@ƒ‚k̺¾‚¿€¾ ÀÁÿÀ¨ !Ž  " )Ÿ…ÿýÿ& +ºÇ¸Ã¾¾Š¿¾¿ÂÁ½¹¼ÅÍÌ¿©ˆlL7?Rt²¾¾‰¿ ½Á¹Î{!! !*ž…ÿýÿ&‚ "¦Í¸ÁÀ¾‹¿¾¾ÀÂÁ¼¹¹½ÄÌÎÏÉ¿¿ÂµŸ¼ÁÀÀÌÎÎÊ‹¿¾Â¹ÌD!Ž!*…ÿýÿ&ƒ  ‡Ï»¾Â¾¾‹¿¾¾¿ÁÂÂÀ½º¹¹º½½¼ÀǾ€½º¹¹º¾¿À·‚!‘!*…ÿýÿ&„ dÇùÂÀ¾¿€¾¿Á€ÂÁ€À¾¼¿€ÀÂÀŠ¿¾ÁºË‹‚!!*…ÿýÿ&… -®Íº¼Â¿¾¿‚¾€¿ÀÁ¿‚¾Š¿¾Â¹ÍZƒ!!*…ÿýÿ&†  vËÆ¸¿Â¿¾–¿¾”¿À½Ã%„!Ž (ž…ÿýÿ&ˆ5¡ÐÀ¹¿Â¿¾¾©¿¾À¼Æ£ˆ! (ž…ÿýÿ&‰ RµÐ¿¹¿ÂÁ¾¾§¿¾Â¸Îh… Œ #Ÿ…ÿýÿ&k¹Ï¿¸¼ÁÂÀ¾¾Ÿ¿¾¿¾ÁºË:ˆ!‹ # …ÿýÿ&Œl¹Ïƺº¿ÂÂÀ€¾—¿€¾¿À¿À½Ã­ ‰€ Œ …ÿýÿ&[¡ÊÌÀ¹º¾ÁÂÁÀ‚¾Œ¿‚¾ÀÁÂÁ¾€¿¾Â¹Í€‡ …ÿýÿ&5¸ÍÌÀº¹»¾€ÂÁÁ€¿†¾¿¿ÀÁ€ÂÁ½»¹»Á€¿¾Â¹ÌJ†"Ž …ÿýÿ&‘H†¶ÊÎÇÀº¹¹»¼¿ÀÁ†Â ÀÀ¾¼º¹¹»ÂÉÏÆ²‚¿¾¾‡!Ž …ÿýÿ&“€€ :sšµËÎÏÉž½¼†¹½¼ÁÆËÏÎǯ“X/™ÉºÃºÉ•‰    ‰ …ÿýÿ&•€ 8Ss’¥ºÁÅÏ̓ÎÏÁ´¢ŠkL/<ȺĸÎ]†$€ ‚    …ÿýÿ&—€ƒ-LFI‚HI"„Ì»¾Æ/… €ƒ  …ÿýÿ&™€‹)źǡ€„!!‚#"$  …ÿýÿ&œ€„†„ qÇÉv”  Ÿ…ÿýÿ&¿µÈ9ˆêÒÜØÙØ€ÙÚÙÙØ××ÖרÚÜÚÖñ…ÿýÿ(©†‰kÉ  ÿöÿþ¤ÿýÿ&Ào ÿòÿú‘ûúƒûýþÿýÿðž ‘Ÿ ¨¡ž ‰Ÿ  ŸžžŸ¡€¢ œšŠ™šœ €¢ žžŸŸ  Ÿ¢—ÛÿüÿþÿûÿÙ‘'– „  ŸÿöÿþÿûÿÚ   @` „  3>KTY]‰^_[XTK>.  £ÿ÷ÿþÿûÿÙ’@P>‹ '=IOQPNKJ‹H IJKNPQOG8&ƒ ŸÿöÿþÿûÿÙŽ  PMW33IUVSPONNOPPŒQPPONNOQTWSF4 Ÿÿõÿý˜þ‚ÿûÿÙROMS *ETUQLLNOPOPOMLMQURC& ŸÿöÿþÿûÿÙUMPOL 6PUQLLN€PšOPPONLMRVM7 œÿíú˜ö÷‚ÿûÿÙ.ULQMS=?TTNLN€P OPPONLOVM?žÿöÿþÿûÿÙ9TMPPLV$ BSSLMOPP¦O PPOMLXIrvU`b^_’`_cZxÿþ€ÿûÿÙ‘FQNONPTLMPP¬OPPLOH;•&ÿý€ÿûÿÙ ONƒONMPP°O PORW^Q'    .ÿý€ÿûÿÙSMPƒOP”O‡P“OMKHLQ3‘&ÿý€ÿûÿÙ#ULP•OPOO€NMLLM€NOOPO PPQONUN‹'ÿý€ÿûÿÙ1ULP’OPPONMLLNNQRRSUUSRRQNMLLMNOPPŽOPMOU.Œ&ÿý€ÿûÿÙSMP…OPLT#ÿü€ÿûÿÚ!!† "‰ ƒ! ‚ „ ƒ! €†! …PN†OPOOI&ÿý€ÿûÿÚ !!† "‰ ƒ! !Š ‚†! ….ULP…OPMS9)ÿý€ÿûÿÙ‹ Š ƒ!  ‰!†! „ LˆOPMS#)ÿý€ÿûÿØ!ˆ "ˆ ƒ! ƒ ‰ †! „&VLP‡OPJ 'ÿý€ÿûÿØ#!‡ !‰ ƒ! ‚!‡ „†! …FQN†OPMU6&ÿý€ÿûÿØ!ˆ !# ‡ ƒ! ‚‰!‚†! ƒ+UMP†OPMU%ÿý€ÿûÿÙ!ˆ !‡ ƒ! ƒ ˆ„†! ‚ ‹OPK&ÿý€ÿûÿÙ#ˆ " † ƒ! ƒ!…!ƒ†! ‚2QLQ†O PMT3'ÿý€ÿûÿÙŠ !‡ ƒ! …†! ƒ†! MVOLOP„O PMR'ÿý€ÿ ûÿÙ$‡ !#… ƒ! „!……†!  ,LUOLOPO PMR?&ÿü€ÿ ûÿÙŠ † ƒ! „ !ƒ!„†"!  ,LUOLOPOOPMS&ÿý€ÿ ûÿÚ  $"‡ !"„ ƒ! † ƒ! „†*! " ,LUOLOQNS?%÷ôöüÿûÿÙ€Š " ƒ„ …!‚ˆ†" 8ªc ,LUOMLS&ÿý€ÿ ýÿðž ž ž¡5"‰ „!ƒ # … ! #…† $# ;¥’¥^ ,MST@^eTÃ…ÿþþ5#ˆ "‚„ ‡‚‡† <¤•œ”¦^€  +N Ÿ€ÿþ€ö÷ô÷&%!ˆ "º;¤•š˜›“§^ƒ£…ÿýÿ%&!‰ " ¸;¤•š™™˜œ“§^€ƒ Ÿÿ€þÿüÿ'‹ " ¶;¤•š™˜œ“§^‰  …ÿýÿ' Œ " ¶;¤•šƒ™˜œ“§^ˆ Ÿ…ÿýÿ&#!Š " Ÿ;¤•š…™˜œ“§^‡ Ÿ…ÿýÿ&!‹ " Ÿ€‹;£•š‡™˜œ“§^† Ÿ…ÿýÿ& "›‰9¥•š‰™˜œ“§^… Ÿ…ÿ ýÿ&#!‹ #›€‚$©“œ‹™˜œ“§^„  …ÿ ýÿ&#!‹ "—  #"  ‚\§“›˜Š™˜œ“§^ƒ  …ÿ ýÿ&!Œ !!– !"#" €]¦”œ˜Š™˜œ“§^ ¤…ÿ ýÿ& Œ  # ”"!## €^¤“œ˜Š™˜œ“§^JÛ…ÿ ýÿ& ""€“" € "#! e¦’—Š™˜œ“§^qœÓ…ÿ ýÿ& #Ž #!€’„  !#"}¢”œ˜Š™˜œ“§^‚¬€·…ÿýÿ&€ "  #! €€#!„ € "# }¢”œ˜Š™˜œ“§^9žšœ‰·…ÿýÿ& "  #"…„!‰  $~¢”œ˜Š™ ˜œ’©RN¢›©o¢…ÿýÿ&‚ "  !#! "Œ ~¢”œ˜Š™ ˜›” Adª“œ”¦qŸ…ÿýÿ&ƒ"‘  !##  €€  "~¢”œ˜‹™ š˜¢¡”™œ«W˜…ÿýÿ&„#“ € !"##"€ € €#"Ž " ~¢”œ˜Œ™•–›˜œ®O“…ÿýÿ&…#• ‚ ! ‚Ž € ~¢”œ˜‹™›š˜˜œ­>”…ÿýÿ&†!! • "€ ~¢”œ˜Š™˜˜™˜®'“…ÿýÿ&ˆ#´ " ~¢”œ˜Œ™˜œ¬"”…ÿýÿ&‰"!µ ‚ ~¢”œ˜Œ™›”¢š…ÿýÿ&Š # ± !… ~¢”œ˜‹™›”£™…ÿýÿ&Œ# ­ !#… ~ ”›˜‰™š˜š …ÿýÿ& # « " † ¡–š˜ˆ™š˜šŽ …ÿýÿ& #!© !‰„—š‡™˜›–Ÿt£…ÿýÿ& "" ¥ "‡P”Š™˜›•¡k¥…ÿýÿ&’€ #" €— €„ " ˆ l¦›Š™˜›•¡Z¤…ÿýÿ&”€ "#! ‚Œ  !"#!„ ˆ†¤“‚˜†™˜›•¡C¥…ÿýÿ&—€  "##"!€ †€ !"##!!!"‡0•œ“šœ›šš™™‚˜›–Ÿ>¤…ÿýÿ&™€  !†#   !!"†d±Ÿ–˜”–—˜šš›š›š– ¢…ÿýÿ&œ€„ … „€" !… kY}‚Ž›š€¤¥ ž›–—“”•–– ¢…ÿýÿ& !!‚„<:]`w…Œž§¤Ÿ¤‚  …ÿýÿ&¦Œ‡""ˆ†97Z\u€Ÿ…ÿýÿ&Á ! ˆêÒÜØ€ÙØÕÖØØÚÛÛÞÛÚ×ÓÒ€ÍÌÕ×âîÚÖñ…ÿýÿ(À#  ÿöÿþ¤ÿýÿ& ÿòÿúˆûü€ûúúøùúûýûûýþÿýÿðž Ÿž œ¯£ ‰Ÿ   ŸŸ¡£¤£ œš•‰”“–šœ¡£¤£¡žžŸ  Ÿ¢—ÛÿüÿþÿûÿÙ Q<’.GCE‚DE%"‹ ŸÿöÿþÿûÿÚ   k¤$  ƒ  0Th~Ž˜Ÿ¢‡¡ ¢œ—Ž}iK1  £ÿ÷ÿþÿûÿÙ’t’s‡€ $Iq†‘•“Ї…Š„ †ˆŠ“•‘ƒhG€  ŸÿöÿþÿûÿÙŽ ‰šZ+Y–™”ŽŠ‰ŠŒŽ‹ŽŒŠ‰Š•š’|Z! Ÿÿõÿý˜þ‚ÿûÿÙ"’ŒŠ”4J}–—‰ˆ‹Ž€ŽŒŒŽ€Ї‹‘˜“wC ŸÿöÿþÿûÿÙŽ8˜ˆŽˆ'a˜‘‰ˆŒŽ€Œ’€ŒŽŽ‹ˆŠ’™Šaÿìù˜ö÷‚ÿûÿÙŽR˜ˆ‘‰”l,q––ŒˆŒŽŒŒœŒŽŽ‹ˆ˜jžÿöÿþÿûÿÙf–‰Žˆ™Av”•‰‰ŽŽŒŒ¢ŒŽ‰‰šŠƒQ`d^_’`_cZxÿþ€ÿûÿÙ‘}‘‹ŽŽŒ•ˆŠŒ¨ ŒŽˆŒŠ|<”&ÿý€ÿûÿÙŒƒ‹ŠŽŒŒ”ŒŒ“ ŒŒŽ”¡Š> Ž   .ÿý€ÿûÿÙ)”Š‚ŽŽŒŽ€ŒŽŽŒŒ ŒŒŠ„‹”^ &ÿý€ÿûÿÙ>™ˆŒ‚ŒŒŒŽ€ŽŽ€‹ŠˆˆŠ€‹ŽŽŽŽŒ‹ ŽŽŠ—Š2‹'ÿý€ÿûÿÙX˜ˆŒŒŽŽŒ‰ˆˆ‹Œ‘’‘”˜˜”‘’‘ŒŠˆˆŠŒŽŽŒŠ Œ‰—SŠ&ÿý€ÿûÿÙl•ŠŒŒ%ŒŒŽŽŠˆŠ–˜˜Ž|v{kWWk{v|’™˜•Šˆ‹ŽŽŒŒ‰ Œˆ™q‰&ÿý€ÿûÿÙ~‘‹Ž‹Œ‹ˆŒ”™“eI3…!8Pl“˜“Œˆ‹Œˆ Œˆ“‚ˆ&ÿý€ÿûÿÙŽŒ މ‰’˜ŽnJ!‚€€‚  Mu™’‰‰ŽŒ‰ Œ‰’‰(‡&ÿý€ÿûÿÙŒ)”Š‹މЗ•xB‡ Kx•—‹‰ŽŒˆ ŒŒŒ˜6†&ÿý€ÿûÿÙŒ=™ˆŒŠ ‹–’`'††€,f—‰ŠŒ‡ ŒŽ‰—7…&ÿý€ÿûÿÙŒX—ˆŒŠb€‹‚ a””ˆŒ‡ ŒŽ‰—6„&ÿý€ÿûÿÙŽs“ŠŽˆŽŠ“-• (r˜Ž‰ˆ ŒŽ‰˜2ƒ&ÿý€ÿûÿÙ „‘Œ€Œ„ ŒŠ•]›€ ;‡–ˆŽŽŒ† ŒŽŒ‹‚&ÿý€ÿûÿÙˆ‹‰ŽŽ€Ž€Œ Œ‰œAž dš‹ŒŽŒ†ŒŽŒˆƒ&ÿý€ÿûÿÙ‡€ 4¡ŽŠˆˆ‰‹Ž€ŒŠ¢ ?ŽŠŒ†ŒŠ’sƒ&ÿý€ÿûÿÙ‡!es‡‘—™–‘Šˆˆ‰‹Ž”y£ ‚’ˆŒ†Œˆ™]‚&ÿý€ÿûÿÙ…$+ 5Pez‰“˜˜–‘Šˆ‡‹†—V¢ s™ˆŒ†Œˆ—<&ÿý€ÿûÿÙ„ )),("8Ofz‰“˜˜•”Š“)¥ e–‰Œ‡ŽŒŽ€&ÿý€ÿûÿÙ… #+((),%€‚ "8Ofz‰”‘¥ W™‰Œ…ŒŽŠ”n€&ÿý€ÿûÿÙƒ ,(**)(*,$ …ƒE4¦ W–ˆŒ…Œ‰˜C&ÿý€ÿûÿÙ‚ +(*€) *)(++! „°m—ˆŒˆ Š&ÿý€ÿûÿÙƒ%*„)*((+,ƒ€„ª u”ŠŽŒ„Œ‰–`&ÿý€ÿûÿÙ-(*…)*((,*‡€°ˆŽˆ ŽŠ”,&ÿý€ÿûÿÙ€ *(‰)*&, Ь+”ŠŒ… ŽŠ”r&ÿý€ÿûÿÙ€,(*‡)*(+"ÀM˜ˆŒ„ Œ‰•0&ÿý€ÿ ûÿÙ +(Š)*'Š”‡ w“‹Ž… ŽŠ”q&ÿý€ÿ ûÿÙ!+(‰)(+ ‚" ‡! #• ˆ!" $ #‹Ž… Œ‰–0&ÿý€ÿ ûÿÙ+(*‡)*(,ƒ ‡!“" ‡! €Q™ˆŒ„ ŽŠ”q&ÿý€ÿ ûÿÙ,(*‡)(+#„!Š ’"ˆ # ‚ €ŒŽ… ŽŠ“$'ÿý€ÿûÿÙ *‹)+ „!ˆ "“‹ # ?˜ˆŒƒ Œˆ—Z 'ÿý€ÿ ûÿÙ,(*†)*(,…!ˆ "‘"‰ # „~‘‹Ž…ŒŽ‡ &ÿý€ÿûÿÙ'‹)(‡!Š !’!‰ # ‚?˜ˆŒƒ Œˆ˜6$ÿý€ÿ ûÿÙ,(*†)*(,†!ˆ !#!Š # †}‘‹Žƒ Œ‰–c%ÿý€ÿûÿÙ ,(‰)(ˆ!Š !"!‰ # ƒL˜ˆŒ……(ÿý€ÿûÿ؉)*(,‡!Š !!‹ # …ŽŒ…Œ‰•;+þþ€ÿûÿØ,(*ˆ)(‰!‰ !"!‹ # †l”ŠŽƒŒ‰•b+ÿþ€ÿûÿØ,(*…)*(,ˆ!‹ !"!Š # „;˜ˆŒƒŽŒ&ÿý€ÿûÿÙ'‰)(+ ˆ!‹ " !Œ # †ŽŒ†މ” ÿü€ÿûÿÚ*(*†)(+#‹!‹ "‹ "Œ # ‡n”ŠŽƒŒ†œ1ÿû€ÿûÿÚ+(*…)*(,‰!Œ !Œ"Œ # …M˜ˆŒ‚Œ†œOÿû€ÿûÿÚ +(*†)(, ‰!Œ " ‹! # …+•‰Œƒˆ—i$ÿü€ÿûÿØ(‰)*&‹!Œ "‰ " # ‡‰†ŽŠ’}4ýþ€ÿúÿ×,(†)*(+Š!… „ !Š" # ˆ{’‹Ž„Œ…=ûÿúÿÖ .'*…)*(,Š!… ƒ " ‰ƒ !!„ # ˆd–‰Œ… ŒVöÿþÿÿúÿÕ.'*…)*(, Š!ƒ "" "‡ "„ !„ # †Y˜ˆŒ„Œ]õÿþÿÿúÿÕ.'*‡)(Š!‚ !# #!‚ ˆ" !† # †:—ˆŒ„ŒŽyöÿþÿÿúÿÖ.'*…)(*$!‚ !# "‚ " ‡‚ !#!„ # †7™ˆŒ„ ŒŽ€õÿþÿÿûÿÙ",'†)(+‹!‚ !" !‚ "… "‚ !!„ # †*”Š… ŒŽ~õÿþÿÿûÿØ!-'…)*(,‹!ƒ " "ƒ †" !† # ˆŽŒŽ„ŒŽŠšþÿûÿÜ&+(…)*(,‹!ƒ " „ "ƒƒ !"† # ˆŽŒŽ„ŒˆŸÿþ€ÿüÿß(*(…)*(,‹!ƒ # " "ƒ"ƒ † # ˆŽŒŽ„Œ‰ÿþ€ÿûÿß(+(…)*(,‹!ƒ # "„ ‚" "† # ˆŽŒŽ„Œ‰Ÿÿþ€ÿûÿß(+(…)*(,‹!ƒ # ƒ "ƒ„ " † # ˆŽŒŽ„Œ‰ÿþ€ÿûÿß(+(…)*(,‹!ƒ # " ""‚ !† # †1–‰ŒƒŒˆŸÿþ€ÿûÿß(+(…)*(,‹!ƒ #  "„ ƒ!‚ "† # †6˜ˆŒƒŒŽŠšþÿûÿß(+(…)*(,‹!ƒ # !‚ "!‚ " † # †;˜ˆŒ„ ŒŽ~õÿþÿÿûÿß(+(…)*(,‹!ƒ # "‚ !€"! !€† # †Y˜ˆŒ„ ŒŽ€õÿþÿÿûÿÙ",'…)*(,‹!ƒ # !‚ !€! !"† # ˆd–‰Œ„ ŒŽyöÿþÿÿûÿØ",'…)*(,‹!ƒ # €! !"!‚ !† # ‰~‘‹Ž… Œ]õÿþÿÿûÿØ -'†)(+#!ƒ # "‚ !" !€† # ‡Œˆ ŒVöÿþÿÿúÿÕ.'*‡)'Œ!ƒ # „ !!‚ "† # …0—‰Œ„Œ…=ûÿúÿÕ.'*†)(*Š!ƒ # " " "„ † # …T˜ˆŒƒŽŠ’}4ýþ€ÿúÿÕ.'*…)*(,Š!ƒ # "‚ !" "† # ‡t“ŠŽ„ˆ—i$ÿü€ÿúÿÖ-'†)*(,Š!ƒ # ƒ " ƒ " † # †Œ…Œ†œOÿû€ÿûÿØ)(‡)(+"‹!ƒ # €"€ !# #!‚ † # „C™ˆŒƒŒ†œ1ÿû€ÿûÿÙ$*(*†)(*‰!ƒ # € " !" "€† # †n”ŠŽ…މ” ÿü€ÿûÿÚ+(*…)*(,‰!ƒ # ‚!„ !ƒ " €† # …‹Ž…ŽŒ&ÿý€ÿûÿÚ+(*†)(+ ‰!ƒ # "Š !‚† # ƒR˜ˆŒƒŒ‰•b+ÿþ€ÿûÿÙ(‹)ˆ!ƒ # !ˆ !"† # „ˆ‡Œ‰•;+þþ€ÿûÿØ!+(†)*(-ˆ!ƒ # ƒ!‰ !† # ‚D™ˆŒ†…(ÿý€ÿûÿØ-(*‡)*$‰!ƒ # ‚#!† !„† # …}‘‹Ž„ Œ‰–c%ÿý€ÿûÿØ +(‡)*(,‡!ƒ # ‚!ˆ "‚† # L˜ˆŒ„ Œˆ˜6$ÿý€ÿûÿÙ#+(‡)(+!‡!ƒ # ƒ"ˆ „† # ‚ŒŽŒ‡ŒŽ‡ &ÿý€ÿ ûÿÙ-(*‡)(,†!ƒ # ƒ#!„ "ƒ† # ‚Y‘‰‘ŒŒƒ Œˆ—Z 'ÿý€ÿûÿÙŠ)(*"‡!ƒ # …† " ƒ† #  ‰™ˆŒ‚ ŽŠ“$'ÿý€ÿ ûÿÙ-(‡)*(-…!ƒ # „"… …† # O‰˜ˆŒŽŠ”q&ÿü€ÿ ûÿÙ '(*‰)'†!ƒ # „ "ƒ "„† # €O‰˜ˆŒ‰–0&ÿý€ÿ ûÿÚ  ,,(‡)*(,„!ƒ # †!ƒ " „† /# O‰˜ˆ‹”q%÷ôöüÿûÿÙ(Š)(+ ƒ ƒ! …"  ††"! P.O‰™Šˆ•0&ÿý€ÿ ýÿðž ž ž¡=$+(ˆ)*'„" ‚!" $ … " €!" $……!%" $ NEN,O‹•–r^eTÃ…ÿþý9#,(‡)*(+ ‚„ ‡‡† MFIFN,€ M‹, Ÿ€ÿþ€ö÷ôø'.'*‡)*(,¼MFIHIEN, ƒ£…ÿüÿ$0&*‰)(,¸MFI€HIEN,ƒ Ÿÿ€þÿüÿ'$Œ)(,¸MFI‚HIEN,€„  …ÿýÿ' (Œ)(,¶MFI„HIEN,… Ÿ…ÿýÿ&-(*Š)(,Ÿ‘MFI†HIEN,‰ Ÿ…ÿýÿ&$+Œ)(,Ÿ€MFIˆHIEN,ˆ Ÿ…ÿýÿ' )+›‰NFIŠHIEN,‡ Ÿ…ÿ ýÿ&-'*‹)(,›€„PEIŒHIEN,†  …ÿ ýÿ&,(*‹)(,% —  .,)" ‚+OEIŒHIEN,…  …ÿ ýÿ&"+(*Š)*(**– *(,,+%€,NFIŒHIEN,„ ¢…ÿ ýÿ&(*Œ)*)),# €” +(*,,)  €,MEJŒHIEN,ƒ»…ÿ ýÿ&)(++€“,(+*€()+,*$ 0NEJG‹HIEN,9?¸…ÿ ýÿ&,()*((,*€’')**)(()+,+';MFJŒHIEN,;U2ª…ÿýÿ&€,(Ž)*(),* €€-'*‚)*)€( *,,*#;MFJŒH IEN,KGN6ª…ÿýÿ&,()*Œ) *)(),+&……$*†)**)(()*/ ;MFJŒH JEP&%LJBT* …ÿýÿ&‚+))*) *)((*,*$ ,(*‡) **))$;MFJŒH IFK/PEJDS+Ÿ…ÿýÿ&ƒ (+(*Ž)**(()+,,)$ € &Œ) *(,;MFJŒH IHMLFHKBUœ…ÿýÿ&„$,(‘)*)€()+€,+€)'"(€),*Œ)*(,;MFJHFGIHJBVš…ÿýÿ&…,()*‘)**)‚(€)*+)‚(Ž)'‚;MFJŒHIIHHJBVš…ÿýÿ&†+*(*”)*)()€*)*(,‚;MFJHJAVš…ÿýÿ&ˆ %,((*²)*(,ƒ;MFJŽHJBUš…ÿýÿ&‰++()*²)*„;MFJHICQ…ÿýÿ&Š  #,*()*®)(+#‡;LFIŒHICQ…ÿýÿ&Œ ',)()*«)*(,‡;LFIŒHFGŸ…ÿýÿ& (,*((**¨)*(, ˆ=LGI‰HIHFG …ÿýÿ& (,+(()**¦)*%‰>JG‰HIGI;¡…ÿýÿ&#+,*(()**š)*ƒ)*(,‰&F‹HIGJ7¢…ÿýÿ&’€ (,,*€()€*)€*)€()*)*(,ˆ3NI‹HIGI/¢…ÿýÿ&”€'+,+*‚(€)†*€)‚(*+,*&„)‡?NEŒHIGJ$¢…ÿýÿ&—€  !'+,,++€)†())*+€,*%  !+(*(+ ‡FJEJH„I„HIGI"¢…ÿýÿ&™€  $()*†,))'#  +(*',‡0S€JFGF€GHH„ID¡…ÿýÿ&œ€„ …„ ,()+ …  2*:=BHGMKJIFFEFFGD¡…ÿýÿ& €Ž *(+#„ -.9?CKJPNLO<  …ÿýÿ&¦Œ‡++‰ˆ  )*6;C9Ÿ…ÿýÿ&Á &* ˆêÒÜØ€ÙØ×רØÙÚÚÛÚÙØÖÖ€ÓÒ××ÜâÚÖñ…ÿýÿ(¿-  ÿöÿþ¤ÿýÿ&Àÿòÿúû…úûüûûýt8mk@     {m  4Mau‚Ž€r_J1  žÿ  3e•¿à÷ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿóܺŽ^+ ¿ÿ´ +p±åÿÿÿÿÿÿûùöööööööööööùüÿÿÿÿÿýà¨e# éüÿ| 0‚Ðýÿÿÿûööùüþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþûøö÷ýÿÿÿúÇy& (þþûý;kÉýÿÿüöøüÿÿÿÿÿÿÿþþþþþþþþþþþþþÿÿÿÿÿÿÿü÷÷þÿÿú¾] PÿúþÿÕ (•îÿÿüõúÿÿÿÿÿþþþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþþþÿÿÿÿþùöýÿÿæ… wÿõÿöÿš-©ûÿþ÷ùÿÿÿÿþþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþþÿÿÿÿøøÿÿô˜!¢ÿ÷ÿÿ÷ÿU«øÿüöýÿÿÿþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþÿÿÿüöýÿö’Ëÿúÿÿýÿìíÿùøþÿÿþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþÿÿþ÷üÿîréÿýÿþÿýüý÷ÿÿÿþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþþþþþþþþþþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþÿÿþøüÿÒ;.ÿüÿÿÿÿÿýýÿþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþÿüøýþTÿøÿþÿÿÿþÿþÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþÿÿÿÿÿÿüúùøöööö÷ùûýÿÿÿÿÿÿþþÿÿÿÿÿÿÿÿÿÿÿÿÿþþÿûøÿÕ2 }ÿöÿþÿÿÿþþÿÿÿÿÿÿÿÿÿÿÿÿþþÿÿÿÿûööùþÿÿÿÿÿÿÿÿÿÿÿÿþùö÷ûÿÿÿÿþþÿÿÿÿÿÿÿÿÿÿÿÿþÿþùýûk©ÿ÷ÿþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþÿÿÿþ÷÷ýÿÿÿÿúçÔ¿±§žž¨²ÀÖèüÿÿÿÿýöøþÿÿÿþÿÿÿÿÿÿÿÿÿÿÿþþÿüøÿ Ðÿúÿþÿÿÿÿÿÿÿÿÿÿÿÿÿþÿÿÿø÷ÿÿÿûÕ oC" $Er£×üÿÿÿ÷ùÿÿÿþÿÿÿÿÿÿÿÿÿÿÿþÿÿöÿÇíÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþöüÿÿï­c$ (g²òÿÿüöþÿÿþÿÿÿÿÿÿÿÿÿÿþþÿ÷ÿß'2ÿüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþ÷ýÿû¼]  cÁýÿý÷þÿþþÿÿÿÿÿÿÿÿÿÿýÿùþì5Xÿ÷ÿþÿÿÿÿÿÿÿÿÿÿÿÿÿýÿò–+  0õÿüøÿÿþÿÿÿÿÿÿÿÿÿÿýÿûüó9 „ÿöÿþÿÿÿÿÿÿÿÿÿÿÿÿÿ÷‹  “÷ÿùúÿþÿÿÿÿÿÿÿÿÿÿþÿüûó8¬ÿ÷ÿþÿÿÿÿÿÿÿÿÿÿÿÿù-#«ÿýùþþþÿÿÿÿÿÿÿÿÿþÿüýî-ÕÿûÿýþþþþÿÿÿÿÿþÿùÿŒ EÚÿøýÿþÿÿÿÿÿÿÿÿÿþÿúÿâéûûÿÿÿÿÿÿÿþþþþþÿÿûÿN ÿúûÿþÿÿÿÿÿÿÿÿÿþÿùÿÌ ;ÿûÿýùööùüÿÿÿÿÿÿþÿüýçIëÿùÿþÿÿÿÿÿÿÿÿÿýÿöÿ© (š¼ãúÿÿÿÿÿþùööùüÿþÿùÿ´Çÿ÷ÿþÿÿÿÿÿÿÿÿÿýÿõÿz Eìs "EošÁãûÿÿÿÿÿýøôúòÿq ¦ÿöÿþÿÿÿÿÿÿÿÿÿþÿøÿDæÿÿá` %JrœÄçýÿÿÿÿþö/ÿöÿþÿÿÿÿÿÿÿÿþÿüÿÞ¶ÿõûüÿÒL  (Lv¡ÉæÿÛ †ÿöÿþÿÿÿÿÿÿÿÿþÿ÷ÿ¡ uÿöÿþûùþÿÁ9 %], †ÿöÿþÿÿÿÿÿÿÿÿþÿøÿS.öüÿþþÿÿúùÿþ¯)   •ÿöÿþÿÿÿÿÿÿÿÿÿüÿݾÿùÿþÿþþÿÿùúÿù™ °ÿ÷ÿþÿÿÿÿÿÿÿþÿ÷ÿŠjÿöÿþÿÿÿÿþþÿÿøûÿðˆ Òÿûÿþÿÿÿÿÿÿÿÿÿüø,æÿýÿÿÿÿÿÿÿÿþþþÿðÿž-óýÿÿÿÿÿÿÿÿÿþÿøÿ©ÿ÷ÿþÿÿÿÿÿÿÿÿþÿøÿ­ cÿ÷ÿþÿÿÿÿÿÿÿÿÿúÿ=+÷ýÿÿÿÿÿÿÿÿÿÿþÿûÿÓ¬ÒÇÌÉÊÊÊÊÊÊËÉͼ‚ØÄÍÉÊÊÊÊÊÊÊËÇÏ*«ÿøÿþÿÿÿÿÿÿþÿøÿ±ÿ÷ÿþÿÿÿÿÿÿÿÿÿÿüö0ÙÿûÿþÿÿÿÿÿÿþÿùÿQåÿüÿþÿÿÿÿÿÿÿÿûÿ5éÿþÿÿÿÿÿÿÿÿÿÿûÿ8-øýÿÿÿÿÿÿÿÿÿÿþÿ÷ÿl Ôÿöüùúúúúúúùýóÿ¦Pÿóýùúúúúúúúúüöÿ4jÿ÷ÿþÿÿÿÿÿÿþÿøÿ£–ÿ÷ÿþÿÿÿÿÿÿÿþÿøÿ³Ùÿûÿþÿÿÿÿÿÿÿÿþÿì¥ÿøÿþÿÿÿÿÿÿÿÿÿûÿ5Èÿúÿþÿÿÿÿÿÿÿÿþõ$ïÿþÿÿÿÿÿÿÿÿÿÿÿýò'Øÿúÿýþþþþþþþýÿ÷ÿ\ëÿüÿþþþþþþþþþÿúÿ5Mÿùÿþÿÿÿÿÿÿþÿöÿwÿöÿþÿÿÿÿÿÿÿþÿ÷ÿ{ Ùÿûÿþÿÿÿÿÿÿÿþÿøÿ°[ÿøÿþÿÿÿÿÿÿÿÿÿÿûÿ5»ÿùÿþÿÿÿÿÿþÿûÿ×ÓÿûÿþÿÿÿÿÿÿþÿüÿÙ Ùÿûÿþÿÿÿÿÿÿÿÿÿþÿñ°ÿøÿþÿÿÿÿÿÿÿÿÿÿûÿ5MÿùÿþÿÿÿÿÿÿþÿùÿGFÿùÿþÿÿÿÿÿÿÿþÿ÷ÿbÙÿûÿþÿÿÿÿÿÿÿÿþÿøÿfñÿþÿÿÿÿÿÿÿÿÿÿÿÿûÿ5Êÿúÿþÿÿÿÿÿþÿ÷ÿ›žÿ÷ÿþÿÿÿÿÿÿþÿûÿÒÙÿûÿþÿÿÿÿÿÿÿÿþÿùÿ¹gÿøÿþÿÿÿÿÿÿÿÿÿÿÿûÿ5hÿ÷ÿþÿÿÿÿÿþÿýÿâ æÿýÿÿÿÿÿÿÿÿþÿøÿfÙÿûÿþÿÿÿÿÿÿÿÿÿÿÿÿö#ºÿùÿþÿÿÿÿÿÿÿÿÿÿÿûÿ5ëÿýÿÿÿÿÿÿÿþÿùÿDLÿùÿþÿÿÿÿÿÿþÿýÿáÙÿûÿþÿÿÿÿÿÿÿÿÿþÿ÷ÿp%÷ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿ5 ÿøÿþÿÿÿÿÿþÿ÷ÿˆ–ÿ÷ÿþÿÿÿÿÿÿþÿ÷ÿ‰ÙÿûÿþÿÿÿÿÿÿÿÿÿþÿùÿÁsÿ÷ÿþÿÿÿÿÿÿÿÿÿÿÿÿûÿ5LÿùÿþÿÿÿÿÿþÿúÿÇ×ÿûÿþÿÿÿÿÿÿÿÿýý0Ùÿûÿþÿÿÿÿÿÿÿÿÿÿÿÿþû,Äÿùÿþÿÿÿÿÿÿÿÿÿÿÿÿûÿ5éÿýÿÿÿÿÿÿÿÿÿÿô0ÿüÿÿÿÿÿÿÿÿþÿùÿÂÙÿûÿþÿÿÿÿÿÿÿÿÿÿþÿ÷ÿz.ýýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿ5¯ÿøÿþÿÿÿÿÿþÿùÿOhÿ÷ÿþÿÿÿÿÿÿþÿ÷ÿuÙÿûÿþÿÿÿÿÿÿÿÿÿÿþÿúÿÉ~ÿ÷ÿþÿÿÿÿÿÿÿÿÿÿÿÿÿûÿ5nÿ÷ÿþÿÿÿÿÿþÿ÷ÿƒ ÿ÷ÿþÿÿÿÿÿÿÿÿýþ.Ùÿûÿþÿÿÿÿÿÿýÿÿÿÿÿÿüÿ4Íÿúÿþÿÿÿþÿÿÿÿÿÿÿÿÿûÿ56ÿüÿÿÿÿÿÿÿþÿøÿ³ÐÿúÿþÿÿÿÿÿþÿúÿÑÙÿûÿþÿÿÿÿÿÿþþÿÿÿþÿ÷ÿ‚8ÿûÿÿÿÿÿÿýÿÿÿÿÿÿÿÿÿûÿ5çÿýÿþÿÿÿÿþÿûÿÛóÿþÿÿÿÿÿÿÿþÿ÷ÿ–ÙÿûÿþÿÿÿÿÿÿìýÿÿÿþÿûÿÒˆÿ÷ÿþÿÿþÿôûÿþÿÿÿÿÿÿûÿ5Âÿùÿþÿÿÿÿÿÿÿÿö;ÿûÿþÿÿÿÿÿÿþÿ÷ÿ]Ùÿûÿþÿÿþÿûÿ’ëÿüÿÿþÿûÿ=Öÿûÿþþÿùÿãÿûÿÿÿÿÿÿûÿ5šÿ÷ÿþÿÿÿÿÿÿÿûÿ6aÿ÷ÿþÿÿÿÿÿÿÿÿýÿ,Ùÿûÿþÿÿþÿõÿ\²ÿöÿþþÿ÷ÿŒBÿúÿþÿÿþÿöIìÿüÿÿÿÿÿÿûÿ5 uÿöÿþÿÿÿÿÿþÿ÷ÿV †ÿöÿþÿÿÿÿÿþÿüÿæ ÙÿûÿþÿÿþÿóÿN^ÿóÿþþÿüÿÚ“ÿ÷ÿþþÿøÿºûþÿÿÿÿÿÿÿûÿ5Wÿ÷ÿþÿÿÿÿÿþÿöÿo ¤ÿ÷ÿþÿÿÿÿÿþÿùÿÇÙÿûÿþÿÿþÿõÿYþûÿÿÿþÿùÿE ßÿüÿþþÿöÿd þüÿÿÿÿÿÿÿûÿ5Aÿúÿþÿÿÿÿÿþÿöÿ‡ »ÿùÿþÿÿÿÿÿþÿ÷ÿ¤Ùÿûÿþÿÿþÿ÷ÿf¼ÿúÿþþÿ÷ÿ—Jÿùÿþÿÿÿÿôùÿÿÿÿÿÿÿÿûÿ5/ÿüÿÿÿÿÿÿÿþÿöÿ› Ñÿúÿþÿÿÿÿÿþÿöÿˆ Ùÿûÿþÿÿþÿ÷ÿgfÿùÿþþÿýÿâ Ÿÿ÷ÿþþÿùÿ°#ôÿÿÿÿÿÿÿÿûÿ5%ÿýÿÿÿÿÿÿÿþÿ÷ÿ©Ýÿûÿþÿÿÿÿÿþÿöÿo Ùÿûÿþÿÿþÿ÷ÿaæÿýÿÿþÿùÿMèÿýÿÿþÿùÿX#óÿÿÿÿÿÿÿÿûÿ5÷ÿÿÿÿÿÿÿÿþÿøÿ° çÿýÿþÿÿÿÿÿþÿ÷ÿ]Ùÿûÿþÿÿþÿöÿ\œÿøÿþþÿøÿ¡Uÿøÿþþÿýÿâöÿÿÿÿÿÿÿÿûÿ5öÿÿÿÿÿÿÿÿþÿøÿ»íÿýÿÿÿÿÿÿÿþÿøÿRÙÿûÿþÿÿþÿöÿ]Eÿùÿþþÿýÿêªÿøÿþþÿ÷ÿ•÷ÿÿÿÿÿÿÿÿûÿ5÷ÿÿÿÿÿÿÿÿþÿùÿ¼ìÿýÿÿÿÿÿÿÿþÿøÿOÙÿûÿþÿÿþÿöÿ_ ÜÿüÿþþÿøÿWïÿþÿÿþÿúÿ?÷ÿÿÿÿÿÿÿÿûÿ5&ÿýÿÿÿÿÿÿÿþÿøÿ»ìÿýÿÿÿÿÿÿÿþÿøÿMÙÿûÿþÿÿþÿöÿaŒÿ÷ÿþþÿøÿ«aÿøÿþþÿûÿÓ÷ÿÿÿÿÿÿÿÿûÿ51ÿüÿÿÿÿÿÿÿþÿøÿ°íÿýÿÿÿÿÿÿÿþÿøÿQÙÿûÿþÿÿþÿöÿ`9ÿüÿÿþÿþÿï¶ÿøÿþþÿ÷ÿ öÿÿÿÿÿÿÿÿûÿ5Dÿúÿþÿÿÿÿÿþÿ÷ÿ©êÿýÿÿÿÿÿÿÿþÿ÷ÿXÙÿûÿþÿÿþÿöÿ`Êÿúÿþþÿøÿa!ôÿÿÿþÿÿýý/öÿÿÿÿÿÿÿÿûÿ5[ÿ÷ÿþÿÿÿÿÿþÿöÿ› áÿüÿþÿÿÿÿÿþÿöÿg Ùÿûÿþÿÿþÿöÿ`wÿ÷ÿþþÿøÿµmÿ÷ÿþþÿùÿÀöÿÿÿÿÿÿÿÿûÿ5 zÿöÿþÿÿÿÿÿþÿöÿ‡ ×ÿûÿþÿÿÿÿÿþÿöÿ~ Ùÿûÿþÿÿþÿöÿ`'øþÿÿþÿÿÿô ¿ÿùÿþþÿ÷ÿköÿÿÿÿÿÿÿÿûÿ5 ÿ÷ÿþÿÿÿÿÿþÿöÿo Äÿùÿþÿÿÿÿÿþÿöÿ›Ùÿûÿþÿÿþÿöÿ` ¸ÿùÿþþÿ÷ÿl*úþÿÿþÿþÿòöÿÿÿÿÿÿÿÿûÿ5Èÿúÿþÿÿÿÿÿþÿ÷ÿV¬ÿ÷ÿþÿÿÿÿÿþÿøÿ¹Ùÿûÿþÿÿþÿöÿ` bÿ÷ÿþþÿùÿ½yÿ÷ÿþþÿøÿ®öÿÿÿÿÿÿÿÿûÿ5ìÿýÿÿÿÿÿÿÿÿÿûÿ6 ’ÿöÿþÿÿÿÿÿþÿûÿÛÙÿûÿþÿÿþÿöÿ` íÿþÿþÿÿþù)ÊÿúÿþþÿøÿVöÿÿÿÿÿÿÿÿûÿ5=ÿûÿþÿÿÿÿÿÿÿÿÿö mÿöÿþÿÿÿÿÿÿÿÿÿù Ùÿûÿþÿÿþÿöÿ` ¤ÿøÿþþÿ÷ÿt0ÿüÿÿþÿýÿåöÿÿÿÿÿÿÿÿûÿ5vÿ÷ÿþÿÿÿÿÿþÿûÿÛJÿùÿþÿÿÿÿÿÿþÿùÿKÙÿûÿþÿÿþÿöÿ` NÿùÿþþÿúÿÂÿ÷ÿþþÿ÷ÿ˜öÿÿÿÿÿÿÿÿûÿ5·ÿøÿþÿÿÿÿÿþÿøÿ³%üþÿÿÿÿÿÿÿÿþÿ÷ÿÙÿûÿþÿÿþÿöÿ`  ßÿüÿþÿÿýý2×ÿüÿþþÿúÿBöÿÿÿÿÿÿÿÿûÿ5íÿþÿÿÿÿÿÿÿþÿ÷ÿƒßÿüÿþÿÿÿÿÿþÿùÿ¾Ùÿûÿþÿÿþÿöÿ` ÿ÷ÿþþÿôÿr)ÿöÿþþÿûÿÔöÿÿÿÿÿÿÿÿûÿ5YÿøÿþÿÿÿÿÿÿþÿùÿO³ÿøÿþÿÿÿÿÿÿÿþÿïÙÿûÿþÿÿþÿöÿ` ;ÿûÿÿþÿ÷ÿ¼ÿöÿþþÿ÷ÿ‚öÿÿÿÿÿÿÿÿûÿ5­ÿøÿþÿÿÿÿÿÿÿÿÿô}ÿ÷ÿþÿÿÿÿÿÿþÿøÿYÙÿûÿþÿÿþÿöÿ` Îÿúÿþÿÿÿú÷ÿÿÿÿÿÿýý/öÿÿÿÿÿÿÿÿûÿ5òÿþÿÿÿÿÿÿÿþÿúÿÇDÿúÿþÿÿÿÿÿÿþÿøÿ©Ùÿûÿþÿÿþÿöÿ` |ÿ÷ÿþÿÿÿþýÿÿÿþÿùÿÂöÿÿÿÿÿÿÿÿûÿ5uÿ÷ÿþÿÿÿÿÿÿþÿ÷ÿˆêÿýÿÿÿÿÿÿÿÿÿþÿîÙÿûÿþÿÿþÿöÿ` *úýÿÿÿÿÿþþÿÿÿþÿ÷ÿlöÿÿÿÿÿÿÿÿûÿ5ÖÿûÿþÿÿÿÿÿÿþÿùÿD³ÿøÿþÿÿÿÿÿÿþÿ÷ÿhÙÿûÿþÿÿþÿöÿ` ½ÿùÿþÿÿÿÿÿÿÿÿþÿòöÿÿÿÿÿÿÿÿûÿ5\ÿøÿþÿÿÿÿÿÿþÿýÿâ hÿ÷ÿþÿÿÿÿÿÿþÿúÿÇÙÿûÿþÿÿþÿöÿ` gÿ÷ÿþÿÿÿÿÿÿþÿøÿ®öÿÿÿÿÿÿÿÿûÿ5Íÿûÿþÿÿÿÿÿÿþÿ÷ÿ›"öÿÿÿÿÿÿÿÿÿÿþÿúÿCÙÿûÿþÿÿþÿöÿ` ïÿþÿÿÿÿÿÿÿþÿøÿWöÿÿÿÿÿÿÿÿûÿ5^ÿ÷ÿþÿÿÿÿÿÿÿþÿùÿG¹ÿùÿþÿÿÿÿÿÿþÿøÿ°Ùÿûÿþÿÿþÿöÿ` ªÿøÿþÿÿÿÿÿÿýÿæöÿÿÿÿÿÿÿÿûÿ5 Úÿûÿþÿÿÿÿÿÿþÿûÿ×dÿ÷ÿþÿÿÿÿÿÿÿÿÿûÿ:Ùÿûÿþÿÿþÿöÿ` Rÿùÿþÿÿÿÿþÿ÷ÿ™öÿÿÿÿÿÿÿÿûÿ5†ÿôÿþþþÿÿÿÿÿþÿöÿëÿþÿÿÿÿÿÿÿÿþÿùÿ´Ùÿûÿþÿÿþÿöÿ` âÿýÿþÿÿÿþÿúÿCöÿÿÿÿÿÿÿÿûÿ5Ñÿüùúÿÿþþÿÿÿÿÿþõ$šÿ÷ÿþÿÿÿÿÿÿÿþÿùÿNÙÿûÿþÿÿþÿöÿ` •ÿ÷ÿþÿÿþÿûÿÕöÿÿÿÿÿÿÿÿûÿ5gÞÿþøúÿÿþþþÿøÿ£8ÿûÿÿÿÿÿÿÿÿÿþÿûÿÑØÿúÿýþþýÿõÿ_ @ÿúÿýþþýÿöÿƒõÿþþþþþþÿúÿ5 gÞÿþùúÿÿþÿûÿ8¹ÿùÿþÿÿÿÿÿÿÿþÿ÷ÿ€ Ùÿûÿþÿÿþÿöÿ` Òÿûÿþÿÿÿýþ1÷ÿÿÿÿÿÿÿÿûÿ5  bØÿþùùÿøÿ±Mÿùÿþÿÿÿÿÿÿÿÿÿÿûû6Óÿõûøùùøýñÿ]ÿðýøøüóÿÂñúùùùùùùûõÿ3)öŒ ^Ùÿþûöÿ=ÄÿúÿþÿÿÿÿÿÿÿþÿûÿÎ Ùÿûÿþÿÿþÿöÿ` .þýÿÿþÿ÷ÿnøÿÿÿÿÿÿÿÿûÿ5.îûÿ‡ ]ÜûÿªNÿøÿþÿÿÿÿÿÿÿÿþÿ÷ÿ™  Ã¹½»¼¼»¿¶ËF—Ÿ¾¼¼»º¶½¼¼¼¼¼¼½¹À) 4ìýý÷ÿ† ^Ó.ºÿùÿþÿÿÿÿÿÿÿÿþÿöÿj 9óýüÿÿõÿ† ;ÿûÿÿÿÿÿÿÿÿÿÿÿÿÿùþG 9óüüÿþþÿõÿ€ šÿ÷ÿþÿÿÿÿÿÿÿÿÿÿþüò2Bôûüÿþÿÿþÿöÿy çÿýÿÿÿÿÿÿÿÿÿÿþÿüýì. Eùúýÿþÿÿÿÿþÿöÿy aÿ÷ÿþÿÿÿÿÿÿÿÿÿþÿûþì. Eùùýÿþÿÿÿÿÿÿþÿõÿy ¯ÿøÿþÿÿÿÿÿÿÿÿÿþÿüýï=  Oúùþÿþÿÿÿÿÿÿÿÿþÿõÿx æÿýÿÿÿÿÿÿÿÿÿÿÿþÿýûú[  Mþøþÿþÿÿÿÿÿÿÿÿÿÿþÿöÿk Mÿ÷ÿþÿÿÿÿÿÿÿÿÿÿþÿþøÿˆ«K +ûõýÿþÿÿÿÿÿÿÿÿÿÿÿÿþÿöÿk  ƒÿõÿýÿÿÿÿÿÿÿÿÿÿþÿþ÷ÿÀ2ýÿò¸o* …ÿöÿþÿÿÿÿÿÿÿÿÿÿÿÿþþÿöÿk ²ÿ÷ÿýÿÿÿÿÿÿÿÿÿÿþþÿøÿð_ÿ÷ÿþüÒE  ‡ÿõÿþÿÿÿÿÿÿÿÿÿÿÿÿþþÿöÿk Ñÿùÿþÿÿÿÿÿÿÿÿÿÿÿþÿúúÿ¼* îÿ÷÷üÿþÿé©b …ÿöÿþÿÿÿÿÿÿÿÿÿÿÿÿþþÿöÿa  jE åÿûÿþÿÿÿÿÿÿÿÿÿÿÿþÿýùÿù“|ÿ÷ÿþúöùÿÿÿøÅ~7ˆÿõÿþÿÿÿÿÿÿÿÿÿÿÿÿþþÿ÷ÿ^ ˆÿ//ðýüÿþÿÿÿÿÿÿÿÿÿÿÿþþÿùûÿïˆ Ýÿüÿÿÿÿý÷÷þÿÿÿÞ›S“ÿõÿþÿÿÿÿÿÿÿÿÿÿÿÿþþÿøÿ^ ¹ÿï9óüüÿýÿÿÿÿÿÿÿÿÿÿÿÿþÿþ÷ýÿóž6 eÿøÿýþþÿÿÿþùöûÿþÿñ¸s“ÿõÿþÿÿÿÿÿÿÿÿÿÿÿÿþÿÿøÿ^ 9æþþå 9óüûÿýÿÿÿÿÿÿÿÿÿÿÿÿþþÿýöýÿþËr%  Ìÿúÿþÿþþþÿÿÿÿüöøÿþý) ”ÿõÿýÿÿÿÿÿÿÿÿÿÿÿÿþþÿ÷ÿWYøÿõÿÑ1éÿùÿþÿÿÿÿÿÿÿÿÿÿÿÿÿþÿÿþöûÿÿúÈ‚Bdÿøÿþÿÿÿÿÿþþÿÿþÿøÿ¿ ”ÿõÿýÿÿÿÿÿÿÿÿÿÿÿÿþÿÿøÿQƒÿôÿùÿ¹"Ùÿ÷ÿþþÿÿÿÿÿÿÿÿÿÿÿÿÿþÿÿÿúöýÿþÿðÆšpL0  !7Tz¤Òòþÿÿÿÿÿÿÿÿÿÿÿþýÿöÿƒ Ÿÿõÿýÿÿÿÿÿÿÿÿÿÿÿÿþÿýûýÿúüÿõÿ ½ÿöþÿþÿÿÿÿÿÿÿÿÿÿÿÿÿÿþÿÿÿþùöúÿÿÿÿÿÿøëäߨáåïûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþÿúÿ=¡ÿõÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿýúùþþÿöÿ„‘ÿùûÿþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþÿÿÿÿþùööøüÿÿÿÿÿÿÿÿþû÷ö÷úýÿÿÿÿÿÿÿÿÿÿÿÿþÿüÿÞ ¡ÿöÿýÿÿÿÿÿÿÿÿÿÿÿÿÿþÿÿþýÿöÿgZóÿøýÿþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþþÿÿÿÿÿÿÿýüüûüüýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþÿ÷ÿœ¡ÿõÿýÿÿÿÿÿÿÿÿÿÿÿÿÿþþÿþÿøÿN#ÄÿùúÿÿþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþþþÿÿÿÿÿÿÿÿÿÿÿþþþþÿÿÿÿÿÿÿÿÿÿÿÿÿþÿøÿT¨ÿõÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿüÿ3u÷ÿùúÿÿþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþþþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþÿï®ÿõÿþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿù%·ÿýøûÿÿþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþÿøÿ¸®ÿ÷ÿþÿÿÿÿÿÿÿÿÿÿÿÿþÿüÿæ OÙÿþøúÿÿÿþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþÿ÷ÿr¯ÿ÷ÿþÿÿÿÿÿÿÿÿÿÿÿþÿúÿÓhãÿþøøÿÿÿþþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþÿÿÿÿÿÿýý.¶ÿúÿþÿÿÿÿÿÿÿÿÿÿþÿøÿ¹ kÜÿþüõüÿÿÿÿþþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþþÿÿþÿÿþÿúÿÍ Çÿúÿþÿÿÿÿÿÿÿÿÿþÿ÷ÿ¡VÁýÿÿùöûÿÿÿÿÿþþþþþÿÿÿÿÿÿÿÿÿÿÿÿÿþþþþþÿÿÿÿÿúþÿÿþÿ÷ÿˆaðÿÿÿÿÿÿÿÿÿÿÿÿÿþÿöÿ†  0àÿþÿüöøüÿÿÿÿÿÿÿÿþþþþþþþþÿÿÿÿÿÿÿÿÿû÷÷þÿüÿÿþÿúÿC‘ÿüÿÿÿÿÿÿÿÿÿÿÿÿÿþÿöÿi E•Ûÿÿÿÿý÷ö÷úüþÿÿÿÿÿÿÿÿÿÿÿþüùööøþÿÿÿúÏôÿþÿýÿå ÁÿöýÿýþþþþþÿÿÿÿÿÿþÿøÿP 2t±ãþÿÿÿÿÿÿüúùø÷öøøùûýÿÿÿÿÿÿù×£_)£ÿöÿ÷ÿ§/Ýÿóÿÿÿÿÿÿÿÿÿÿÿÿþþþþÿúÿ5 ,X…¬ÎåõÿÿÿÿÿÿÿÿÿÿþñàÅ£zM!*÷üÿøÿ]{ÿüÿÿÿýú÷öö÷ùûýÿÿÿÿÿÿÿÿú   +=JQVaRQG9' ÿöÿò¨ÉÚíýÿÿÿÿÿÿÿÿþûøöõø÷ÿå   ìúÿ¿%>WrŒ¨ÁÚìüÿÿÿþÿûÿÕ {ÿÿ~ #:To‰§¹å« ×ÿ3  kæ ^ic08ÇN jP ‡ ftypjp2 jp2 Ojp2hihdrcolr"cdefjp2cÿOÿQ2ÿR ÿ\ PXX`XX`XX`XXXPPXÿdKakadu-v5.2.1ÿ Æaÿ“ÏÁ öÉ,á‹r›˜GF¸AÕæ¾€Ü—Š ÊÁÔ+zSþÜ( E®3Æ$\\¡Z^½5à[dŽKà·cd!Ì—öºêyôÇÚhÆP¹`EYâ^ðÙÇU³@Lµ vÐܺoP­Ë[,æõ¹µÃ†¬´,cGrnû¯ÅÏÁ 3ÿZßeeªC÷h'ÑGz,†ÒN,yà‡CÝ‹²&zŒ']$h)óž«ð÷ƒ‘‰$ò•¦}!t÷=ÏÁ4€§> à_qPMJîP««Êp~b͈$þ©z4QæÖϰ1¯\ß •,MÂUๅ?¯i{²’×öÿZeø x‘ÅZ¶ÐÛ›vù)^ÇàÜià ’Á:;ŽSíÅsú*»tLƒ9,¯5b`â—ÕoÕÆOg8F£¥RšI)‡9Å@²dGjd›'Ë’~Ô%m}¹]›)› ‚k 6ac¼ÆèkÜ,3Oèz=§W5» ¨*Ûï¾:ßûά k ("ÝÿE{ù•zˆÏ…YE“Þ9'Šì.'¸nÜvLysÙæÒµ¿ØyÚ³û]|—èºóijǙ‚‘h]!JL§¨³eÃí,‡ÚQÚX7еdcSO«³¼u÷éF ä0ãðý€¤iÃ_î3»`K€¸XÕ Ð3àɶ'g„Áê®…!_Kw”BåtÁ“hô3…ZØÙêaW«Lp(moøK¡ÑaAwèGWÜ)$•Lry´®àk ESÂWú”ô"oî ì'mbݲÃí9ÀòØg"k:a…ú’ ÑY²˜GGøªP|Œ4œcT5™‚zrþñAjzJÀ''zbfc¦ŒHÃMâÂ`ËvŒ×špËfòã1%ãgƒ'xk°„ï~üôûZâØGñ°A”ò› ìËÌ^»0P¼÷š -ÄB_š]˜PÇQ$ŠCè¼OV8“ò¹ÌÍ K…OIß©ˆ"‹1Ž•–ð03m¿'èÌF“¢ÏÍ.~iù¥7aêߘҼì8݇k9Lq.å1)(8`™ÌŽÕQÐýàÃGžŒ<{{à•ÆíDßQª!‹Kê?DyˆBB2·…bD!}÷úp›RÃ'g€ë‹¿ˆŽŠg äõ“rhš¦7È.…~‹ÓÝÉ0×ú55¦¹üË.5 ÄèrÆÁµ7ÿe™˜29®üÂ)IIIáYDJ`næÂ}?V‰`U©e®2\Ž…°o†}=Å™–ÿÚ•*3¼kÜãèo ¸úç6Cc¾Èq"B‚À ,Âîlpþ8>Oâé0ß‘vÖ%ÏÎÚ½‡ád€aW& É(?>KÞV=ô ‰æÅ¸x þññß= IJ\ë;ñ‘Ú¿¶?™ìV²žˆ†°ŠÛqõ”é›/?¡HÂáä‚t¹•Ñ“yûóâñ “sÒTkŠg–p}*hUËÝʰvò>Î óO/ý¡|yï'e¯/ôáš]iån¯#!úï½/!ÁÿDõa`’¢pXò¯`¢Iˆ÷È$ ZÓ÷Çä ZÒ¥Q€Öü—Ød’ÿ ,æ˜5>¹¡V°œÚy¡f1$úã hœã#®’‚ÎÓåùhHt—;©V¦!`¨ÔÃô55«tn ëÇÆÀGåz« išb‰ý‘•õ.ç¬ 7VDÅŸÇ.¿ð†Têôñ÷`~@¸%™o\Þ."º"2Xq9ÐÕ±9ŠÊd¶x±•V©°}‘˜vd+5 Üe•(kc±yé„ír#½@dah첩%ú«Ë¬’©jK}Ò%æíáf6mÑ ^›ßÖä܆Ÿèªµì+=° öGîÆ¨ 5žxF€zck€ùG*NÖêny‡j›‚¹·O í6-?ïa ²ÖÉßýÇ塞Ïþä3Ÿ8“uˆm.–HÕ®¡¦¼±1[ð-Õ¤äÃg9ÖaÐàÏmoùoÓ¯g °-L Ñ5ßÚÂ\ÂŒ˜ZvcŸÆŠ šÞJßíÞ]’´`BÞNï#¥•¥ž°r*ù¤¨Ö£zUÖO8ÙVNjÌX3©VÂB…–T Ì0Æ/õ`Ãí¡¡öÐ0}´P‹°£ÿHüÌ©üQŒÖ’KÞ”Ht¼â‹õH˜Ê\pèèÐÀ[…µs>g—eP½ÔÂ'•¿i^‚ÜÅ;¼IpéüÓ-sr¯6(‰œãã$z>ï r.tR>º söt+˜YÓ/$~凮pi ɞӦм=¨U¼¤/tœ>o8J„š[#qy6™œÒv |§Ü ÜÕÅ`hž"Ôáö†² 5ðÍmHê3±Ë'¼iö¤^_+pü2å»YÁ€¡ÒHC…J<á¨èòÙ€U¬%8ú<>˜yRa"8h‰ÚœµÑ/r–G Ößó::ô—îaͯáGgKí ¹Ëÿ; Þ OàǨq–žgݪ¬%yÅX¶Â!¡½øiw±4[º½ÖaßßÚÜõ¬´1oþ_ÏÏ@§çxGç}€‹«@™¤ƒ–k®ö¨$*¼H¬vôrb¼^~ÏÝ·}\­¿O‚Ý0Ä­ð~y}Uo˜]Õ&?ë;äºY1‰¶œvÛ<äÓ6šÁš±bG¯/v6øµÊC™%ƒüÇÚó9V߬Äl}‡o´`†Eïh¦‹UŽ÷œ$­ƒ¢ÞÃaõÊg @îÀye÷`¤´O¬‘å®zFýò‹Ö… å—T× ü*þ»í#Еܓ¥él›$ìëÔhDxHj»öOdrŽþ¹XIwâoµÂ<@çqÞ^èÙh9w׫4›x)P™¬G¤I!OKFŽÀ^çðî)™ F‹ŽÓN‰ºÎ´•“¥ j±TÒ+Gïž°}DÏœ YÐ+±ýdÉ….f¨’WI1ü•æ„2¯çw‡»J£[ã‡J—°ÄR[jþÏQ³q.) ðô#õ£¼\8Yr*‡íú”U9>y€pž½ATŒª˜ky³Dðòx‚šý)žWÞ,Ùu½Î“N¢oƒ€}ò#’A¡{BùA"|×ù\=9ÜËI…›7„¨Ö¼2 zŽtµºb¿tÕ+v¡ÕÞ?@8×<5›Üä^®&i=Ž k‚¦| Èyn†jó êåÁðÀ÷ :Ý»…|œéãVóÀ˜PpÛh[tÓÊ_'u VL.l`ìÛ ­bk¢q¢ =‰„dÏd ©YΤ~žfeQ$o€™¯lÌCüWdܬq(pNò¦6EÈß72”Â2²Þ{¡¥CUl‘s°:?­ šäâ”´ –êS<]Þ•]\¡³¢˜ÌF— ä€e<Y!ÄtÅîWJl޼½øÜ¦÷‘í}üCüLl™ .Y—y“‰ú›²ùЉ †U#“²O—˜L²h‡àTšj‘kvª“S¢¬FѼ†‘s@6ÍZÿg“ã1’Ý0£6Éá(™Â?›)¾O·Æ+‡j³d•pÕ{ 9ÇáÒ$ü÷Ò†÷€ư¾+SƒQYß½XÔý^|¿ “‚£_¥Àƈ•ÿ(RÊø]E!£ UT^•j¥<…Kp˜1!Î)닽ҬÃ&6©’aŒ*ô²±QÖL†ÅÆ›^HÓ…gð6w‹I¡ r(äi·[ׄót…3¹í梗uŒ+ê«$úë” ¥Xû³˜}¡Û£„T‡>= §0#Ó@jT¯æÔ:ÉF\é\¿Ë8²¼3F€H3«(rd œ„r@;rŸG÷îSúVصø³ ¤ÏÏçûrî¶óŠ_ˆ(ß–EÖj%ìû«Y¼‡Or<”f`SNã¡î mÙ½èÓ·_+,€kïqü‚Rðåmð,Ü$ «³ ¤‘ÁÄ\cõØGP†6øÝ{mÚür¢ ˆ½6¢€·ç3oæ²óÝkMÌa4Ü, 4] êpÁ^\™~ãÕ|™ tC<2˜ñ›Kbrm53›0ÂÅ`ÖO8Aݧ÷xTžILxÄs¼èï‘ «¤²»Y hu§©<®âó¦ 6ˆÜ‡q¡÷HF¦UÆ™§ž1ùÝXÚtœ@ùÞ• ‡.,!½£ÐÆË×£õî+5¶¦߸¸ÁêI³a ª­•êtkÀ¬Pt§H§ºA‰õ@¨£…²ðsqåªåÎNHùÇCî^÷ï4ÁÏ$$®7Óà´šÜ*aÕMAõabV*Þ#D~ðñò6£‹ª¸óŠÚï~ñ¢el²žPÔ}§ <§¡€\&Þ pmJfã`øCúe=ª÷Ëg]’Ý ܵâ­Ý ¸P•Ì=Ϭ êÛÛ0Pkcìußʽ*@¼oµSåDÛ£oÇöïa£1ǸÊî³¶^¯¸m@žÉ–Ûçút(à&…/z,Ð kÌHµ\á‰·Ž cc"Ô°ÁWõ¾¦¡\ÏEÐ^¿+%'.E{ôXf~ÔÀ[ŽÑQ&KJ¼¯µL*nbm8ËÕ Ý{å{MHó­>\Ãî³"6a¬2wü1ñîµÓX„Ô²§¬€l\9p>èŠhÜ yUF €\»Ô&¬ÝL‘ú åÇÙ˜UìÁžG`IÆ i¹•WFg¥Y^-÷ÅrºÃnñôÞz.™ E$KEÚøÖò¢"V1b3ŠêÎ<ÓWg¾YygEܳ¤Ø¿ âþ[·Vg£ "!©ý-„ï”ÚÑÕ Îù‹¨¹˜úKv€±oî~α lð@×mc3Š~¯ÓO“Œž²^ÖǪf†ÐÞzlÁEXÄoÀÙZ¯¦¸v¢>ôBI_w&œË÷`:_é¹K “éÎOÖî ²»»`¹ê!Å’ÞUšÃ_ïJb~*H;qŒÍT9÷JÀŸK‡:¿àå:§¢ÁÖPZ®òOŸ&WN†Y¶QØšñ d×6O‚¾¶%±¼Lì½÷= C‹èdG¿röç³ÆqDç­jîûÏűÿ$#4Câê)ö,·B½‘Ž0;¨ß/îC7ú nØö+DA:·¸8ŠE÷ƒ—þ,þò[¯O¦æ_w™û´%Cp°Øþ+Ó¸<”E«*Ü¢´h-äf¯ày±ö³çw‰ö‰ŽøL>ÈlÌD°l|«QÈ'Dtáw¿˜-®¼/R ’¡óMÀ¶˜[#ãƒp±©Ó ·\]¨G(ú¾æÁ¸ÃËx}Ž2tBWq±=ç¤!Û¡ý-påØ'=q‹âܧÿˆpwÝbêf‚ #E¾[ªmް4ä ‘°,¡Èdâ›ERŸü ~2xÌ¢T$l¬ÜXP!Î MÑÂáX°´p;,úÜŽm%ùRÈ;p;WXAÜJ´…Ú!¾pc=½‡·7+®qí¥©º'ôG<”S[ò;¿©»lB*Óxê$‚O5„ðb å;«ªlͬ6¯¦q-ò–ðqÈ¡¤h®Ù{@(¬þãlˆnëðÿ}1¼güB‰kPa„°ºÃÛíŒÏ-z„íÁA (ì"=µŽXîˆx°sŒâíÃL¨4CôNIüˆÃíÔ8}ºmÛ¨ ãUèž„Ã,°[†¨vy‹6XGî$s1çåÒáMŒóHœvŒSñê—gÖÒËV½¦DY]_•ˆ¡Ê>—-^G©1ŽH×ÎŒ÷ÈüÉÍh­}[²P♂@°zF.c/G~ï]¿ÙœáO˸ ¤¦Qµè# ™Óù8ºƒÐgš{´-aÏBí‚U«RÆmÓ×°,âB«/QÁaG‡eYEÔ˜þÖæ]t¨T2`¡˜@d¤kû,ãzW€^Ð+‚À:2#{ðiƒ6û›ðXêñz£Ò(ó èŒ×Áp/ `vÕ»nâU‰=ßyösŸ/Ôû@ôÂÌJ K2(u †úƒÚ,¥³žÁu\%ÆTº^À)9û‚uûçzgÝGõЉŸÑ\Î|ã©x”Löè¼úûÃ|–X„í«r:Éh‹8ŠÍÖ +ã€(NÛbš¤ýfÖŇ…ÍÝCe‡b;¬WÇÔþ˜X~¢5}«ž#-‘Üïéî¢ ¹˜aµÆ-a –9&p3—ÓX$˜¨ÀM–ØO‰÷)ƒ•S>Õë?Ý©=i‘™BLT W¡÷²FºHĦŽëµ¹‚r©ÿpŒ†^éÐA „#°-¯ Ô¸Ä ‚¶VD›ýrÜ’°r]Òtl¬çiÃtÇ2ÿI6%~à5¨ƒ€-¥ôîÛn™)°ßœam¶Ä$<Ϙ%ßAÊp\4èä`T?„,ž„ÔÁ_.àmkú½%á5=ñàb\2屋ö×4y(‘"iî3‰½BL¬WzŸ §;“³™"ÈHp‚®ô|šRY"$Oxy•{Š˜ïéÛ(-’𻊩„³f¼öD®x£Ûe–øý|{!˜[ôó@_¸U1èaÀQü”s­¤\ª+Ŧ¹AUOñªZÈDR¦8…4 6&-ÓÇàÍðáWQÅT»ElÑçY–¨¦yïï“7ãç#Æÿfà_ûÏFœÄ¦ k‹l}kÇÜ:ίH€„•ô0à-v3·~Ñs‚«:O—ñš\£bE€Œù¢ï×±ÞÌÆt”ôÙjª½{¼ü!¯?[!d¨™’¬K˵.Ó¤L;¸l[®›žcá}+gÁŠúXIjÒP-Dþ Têh'šlòý¸@:<\r¬tj± §ðWêT'$Æ€ u>xeõr)k@Q¾àcs!€1ìJål¥·¬#68ªFÇŸôço-X\Y÷Ƕ*Ƕ¯ÇËtZ‚µÿG2óó‘БDáìCÝ=™Øuw ÕÇá¼0ûw †ê€ã‡Æj±¡A7ÍOjN¬!Ó˜r°öxUŒ¹™öü´°ÿgóüŸeNRQñ'É3>àupˆ„NC1ã±ö|zêÒ/ÑXñÝPT"»“Œ-?èØY@%¦gR#¨‰«AÄ,YkcŒÝµ-.&šUÚçs>q8»ïs;³Äbl«h¶Þ²è'ù×C¢–G>¦0A/¯H¬áÎâm]gg‘ªcžq_¢ P†Éæ, ‚8UâòeÛ*yUÏòË‘¶Œ\Ú,ŽªflOÿ|t– =~ÎÏ$èáÃZ$pþA(Ƙ0u¦®QÄÃHJèðp:ö^Æë6‰¦ý¬ØüM¶tÖÄÐz?zÈÇBX*˜áüÏ]_Ík×3+ø™‹ÜØ¡ s1®_.(¯‹2µIº²ÈðåÜ Ð-b›+B‹ÔÓˆµîo¢Âœ;;€WõWrQô¯¼^#±Vƒ•”»…?'7¤ºÊäÝ09K^ Ls·~ôïT­‚J“l¨ÄÊFV[À.†Ü1W¢.Ì=A“—r;$åê ”O¿ª•^á88±òµ¢'-EêÍQäH=³o>Ö5ðô1;†F¢ŒGeR#Ìé£cê™Vó½årÎÑ“(@šÔБºÙ¬»’]›mFtø9éT0­uÿ{3¥ S¾Ví¸Ž',ôf½'u' ¦¬?Ó(uøæëh¥Ù82Mw*Ì«i×¢{`L„ŽéÔÇáö†š‡qsë€#ɺC™²õñ˜ãP•a\vƒFGnÌ’Ëô ·eüºÄÃncÑíí““‹úA4ðÝ϶èò>…‡Nï¯"Z(x°ì`Øè¡e X¹ó݈®ïÃö> ”(…æhS`w‚HÆbRÉÀ@JâwÓÙµÚNl «c¬ÕŠÂ•‘ÕêÅWÕþY\ ³Ñ3~•-HËÀÌ,cÑÆ¤Ž§=9êÓó ×{鯀3Ùm~·…Dœ~M, ‹ëÍ®j¦ÃæÛò, þ^E»Æ nÔ7Ç© ’X&™}¨ÍmŸûó†Îê*4%ˆ¥éq`ß[©÷°í ,œA»FpNŽlF©»Á0'.­Œf€®4&"®œFõnœ)j>bñ:óšk•/ü¼Çjöh#lÈs;Q¨­ËØ ËãJ,êUý˜™óu§oŒgl9•£²ÛÙ÷K¥×Ròê8Nr{ñI4†n¥Á™èQ¡·R™[:`GºÑdüÎÖê)ÒCšš š°ÌcçhiŠ ÃßF(<–)Š‚$Ž`KgAGhqô¦$¹ãÐ䉪ÄNŸPQ¾Âî¼ éÊèÉÄn°Ã Õw©f&‹ÚGÛ-h ?Iq¹åìÕr cýœ¢Ï„ÄíÆº=˜k­dÆ@µ_PDä |sãŒô¬ëÛóS‹ÀÏO…Ù¶ FF„>A3ô_“A„~Êê‹øÛzNãneê‹È £<ߌè¯\d|€ _‹´t¦º!Z˜/Rçâ}·­…C¨çz, ãlOžÓ&qfo3üh”øåÃ7?_Ÿ þöµÎpäeP5‹‹úôæuÎiã¢5­Mµ¦n bÐUáIE–b^Ýf Ø-±I#äz¡®´ÏϬèü:Ϩã…B_Pç¾Æ@ Ø£ÖŠµÜ¡xcd‘nû·æ³Z “ÓÄVèAߌò¤´ ¹Džbíìê‹ ŒPl˜ë!( ¼þ"!¤ÐÇÿdmù¬ÒjpcíPqòú¾Ë~$IÅzp.^<¬ÙªŽˆ:º|X€ÂÍį}(â‡Ç“÷´dÒ0İžÃ.YtV ض†t!ÐT‚v[Ói­…+‚ÈÿB_cn—ÚkÐÐŒÞ ’?߆r~òhý—Á(äwûë ]ߺ3½Õ[¥ô 3!FÞ¢ˆ¨­gã:m'ŠÒ1ØŠsËÇ ý/³)ÈÆ;ΉWtõ½=¥OÏLàð¨é·—¤ùDã5?Õžl2Û¤[óyO•Kñ8$ŸFåšåÍs!ñœ):åV}¬«9,ÿ5NáíshpílôÊ¥ðP"ƒoûÜ9ΘԄÎó–îWQ_ãÅB~Ušx.yÍö.Èx‰½;Ÿ*¬Ðü­Í9XP!=]w<‚.Ä$ }Âo®†ìýSu–VýÃRê„rauÒè?²ÀïÏêJr¦…gÍ3†hcMíŒ:lÇtÝ&°˜E¬” (+)…Dòj Ø¶pÇÈ5æg€Ï=1ÀF gk )å-ËùòÌ„?:Ìv añ£»Ýõ}ë ø­REºÇޱ&ö&Ü61SÊåOf¨ÓëžÏ=Yéî)"ŠÅ$ÀMZ¾ÿB†ïT’Q®ˆœ\[6½Æ‰Éÿ¸€ûDIi­VË;QvK?ÓÃVƹ´Ãz¼+ÇhËzòïHÍ×>š×ÁeYéÝ;_+' +Ƈ2Ý“²6$Á^9dM\Îjƒçæ\Ë&[2·AvOÎhï÷«mÝ·£:EÌ ÷æ¥åÊTbnó_œiçºs%âÉ®ì‡Z±cN@AÏT÷N ž:"+5gŠ=„å½BoW–¦ièÊN¹¢*\—D $_ùPØþg=æ²o÷ç~žáÞ¯ÔjÅcT>d¬{Ú{úõøÆ}©EgáYN¥½À gßÿof:+E úÏùÛV__dß)èý'+¶3Ä:ç&ýýr"k3 ëõÆþTrÕo7.£ÍPzTD›ÚºÆG4ÑÀ¨¾õÖtú¾¦€õ³8F’mð…¬Ãõ#+ú‰‡pƒÖ§Z@¾Ä!‚:ràŒ½E—&•½,tÇyG¥“IË't?J°¤·?¬Ôx%Ö{ÀጹR(ȬpÐ#´hk\ÅW¢çû¯ÅWˆ8þÐÑ€b+G‚;=~èòtƒj-¡¯l¹3fJæ^;®\q¦ïÆmžÄWep÷}Ë,ûÉ(/üÇ[¬<°žWcøASs:³øÌìäì–á.ÝÞ­>¸RÅVÀøàÍÙS|¨0ê؈=áòªºÔ¾d5Tíœ%J`͈>j LQÎüã&¦àåô ´?š‹ÎŸ*‰70¨K+;÷¸çs¤Š¸ZÒºÔ¹,,¢Ž éÒ4†Ré£+_7ôB¶ß™5BÉkb}[óSyH²¿f,”Lå9 –¢¬Nñ *ª{å}>e›{}yà ÙI­1åòi StŒ÷YG)ðWî:Lâ…áT„¼ºç(Øÿ` ™…»[fσúk¦djÍ‹ `ŠœËÓÖm‹K-Ê:·q™Ü±*A;–éã嬳<¥Ÿu^Eº›OpõâØ®¶A |x~Šèa_i‹ù«ÉO p€åäÕ‰l†ab‰|ôÿc— È­²wS´@b Ž5 ¡| àä§ÑyÄÿ„kÒ¯SBßèêµ¾Š”¾*i¹×3:ÜG¥b½eÓu`Þ››àª¶‹•[ÁﲑVš¬á<4…ç2ß¡VáüüVˆ¶¦ŠŽÉ4HÒ‰1)‡-NòE Â/Qcï&,¬H­Ï´ö€¼¨…#äö,èH^¤¿@I¹iéµÉfxb'Sbûá,8 ÑØLrÉÚg†æÀj—ÊÂÆŒ,1‚¸— #¿®´¡À\SæP¨©Œ½ÚoɦÖ3"ȕׄ¾²œó„Á¤Cìª$Ê“çZµØæ’±/êkøûÀ.¢•×ä“£’Ð ”:îj¤Ò;‡ ʼ¸‚Â#½ûewR t¥“P¢ÓÉs–»NÊ3Ú‹1\œÁÁž«sz%kˆHðô$oL¢õm#„ÝŒ4ð¶©ÅN N%m­®Ñér GW¡Û„a޼¾½ç£W°êRÀ¸ ‘µ #Šˆ²¿+(œá:‹€µc10¡ÂÁf!ïgUc4:I€¢(mIìÓ= §ó¸$Ô»&Di8t]øµ—¡|}F‰?ÍÇïÀ¹u¤ìÝhÉÝŒvœ¤ Új²u§µ¶—Z~! ^¹×ú[,ú‘ŠikžÇíîUg‘4üo±™”õ›ˆâƒÔÕùà“P{ð£T¥4ólÃü.åW—ð ÎciƒªSdAfÅü{b“«ˆÕIHN‘ 4£Ãé•î>ŠN³µçÕÌô­LSše/©I¸Ó¬—ü’Âo †§‡Tœ5jË +^íp‘v–§J›~Ñݬ=›Ø™ dmcå1#¥@ÆoÅÅס4œ¹–Ô9*ö‰²?2¿Çá飷ЎoËÇœc>ªB篱ûbÏ´T CPVæÐTöÈ,ÇŠïçËxÎÿ"«¾ê ¦LìÚPá@¾Ká–O(ÕÛ7->Óáî %nÀ±£M‚<ç“P±5=µ€ë>€ß Ö0˜4–Òñ†š‡\LÍв0,Ç“AKîÖÐæ“Vâ¦íVv—¢ûÍ&^gÊ(´ì‰|õlx\’†D"¹O`Ò;0…‰<é®áóÁ4)Ø ¸¶}1[ÖÀ>@¬h%ž‡ù¤~™CD­p0åØûG»ÒZ¼ê6PÕ»Ïi˜¥z£¼0ªø¿^¥ ¬BSõ‹HuX´U¨hlK0:Å'[ Lq÷Å 9gÆ¤ó· ŒºÌ¸á£Ò>VIð!.®£ È„¦\¯™¡êÕŸ<> róïîŠR(ÀÜ»½Ù¨OCü5€^-y…2¦.ì8à>„ÑaÊ{Åï¼ 1Ά#X Ä‹¹C¿‰â4ú(KpsAVógŒöG:ÿMÐ>pg¤ÙðÐ7³IÁMó’2dù Ø'ÊQ.×[ßñ¾ôŠÏúˆ{t¡Ë"¡AH>^€PXòÝ5g.ô£†k"u HËôêø`IK°J#,K@¦t¸êw&fÂ1åÁËR(5·@|Ã-ò/GÕÝii4!Îæú}R~>€!Ñ@X§^ÀíM¿E<é ~è¹æ©þDbÑ3;ô*]]H‘r5Þ½ÆL’l-D[§ëæ;x[yþðꦹž“’LêI)y]ƒÜð6‰9väl9‹¯ãëq†ìâU´? jÒœk{y‚Ÿ"Z‰`6“’ƒp…*`OØè ±©ÖfóþL’ª·ÿº#ð‘¼ããVPfX/UWkp6 ÞÉ W€~Û S‹ÌŸ@·Ø´îéÑâ2h,ªqá¤A%pØrdn{Ù¬uýÞ0v³âRa;åiáÓÚOa §˜úy-f0Ï­æ?ë&gTãñóÞ²åô™ ù<Áý£~„¹CB«ªåН¤~sÞ^Iu=ÑÀ]ôQ!µŒÒ ¯ñèèYòç‘{Ž÷å dd¬½ºcÈG¯ô¢˜*[%³ôLÃf®åCÌO|5(g°hö¹£œ‡2Œ°CÿÅ"ÊO,h-®Ü‘éšó©gØ@~Ö#îHgεp'ôê’žwpG«¾¼*Ž‹Q÷¤H èZšTô#‚J¨1òuëJ¸ôŒpÙ ù®­ç¶˜þ[‚“í…os x‰åd ø’]èßÔ_` Ö9Zmeóûùnãx°‡×è)%«0"É—ŽKü¾S ʃY{$²OŠêƒ5=Fƒ MÅ»n3vuòÑNy/À‘ŒÊÂ[ÌÑÚçµ±ƒ›Êý¾.m~Cê0·øÛqA ·H[VV“õ£;Øøg±ì´Bæ’t  ô&IH [çËD½þlý×i<æ…˜øcb„  U?ÅïòÂÈ!Ñ0~õW²aãM³ÃF'¦!_çTsl~”qc–8k CôÒ‡%ZãŸà~ kü„CcܤÇÿb? we-™g¸bO”Ú;¿ä…Ô`•¼7^ r¨Ù­·ŸÆú°iZÒC"_Sˆñ›£Ð=ܯ#»³53ÁÝ9ì.5åœÈ‡”sIæyü꘣ ƺuSÀÂJdŠõPB="K»©¶¨Ý§Ï ¨ˆ¨K+Ìþµ.—Jæ=aØg£–ãLB4—K[çØ1O²60d.]}æW|j?À-åO“Øåý’Bj lËèu%’dÏp—‰µòž±`ë ²¦œpˇù~+ŠejHJgMkÊ“×(úo<3+ÓŒš-Ã7N)üsüxÿ‡dDmÁä 9…ŸãÒ‚X®×ól3¹ÓýÝ2E‹O®H¾qr'1Ô,4=K°š÷w+P]®pŸÛìsÎZ|/OŠ5¶›ØZk”¨x‰L÷Í^íXç¢]•ÇËbä9š2[JÆqRtõ{{ÊêŸôÉÝÿi²Ü»©õ ŠÊ;½³lžCXC_qJžqº+\ñMÊsgòᨷ~(@Øó¼ˆÙþ©>¶ÏdaÿM¾þ;€qQ"~k:ö Žt‚ù¤µ¶â.¸£KðÛ×™†ª}¹4ƒn<Çæô8Ouàx`0í3ƒDX_Ê_©åŸ)³Tc ׊(>‚ÊJFBÃ"×Lð¬íµú”@â·òùeŒúÜÒ–²7cÀ¿¾E Í‹ù¤ékœ¤oç†õÉ Ø‡Ïl,êdAkݵڀ¬[6à ÂXs79 vWÚÎâ§fg`Keò?§·ñ; ̆å}Ê¢.Û%·;oÐ3ó9vŽ`œÞ5tô¡ îÏš¨Ë|¡­²MãÆ%Æ fZv`dÑÖ]ÙÞ ·½×œsw'Ïçgtï/YìÕö$‰,7vŸ˜ÿR¨ð*º§ËÀãÜŽWæ¾{˜WÆ@Á;thɶ22‘»/Ñ)í:&¬˜“&ƒéViÚÚ^«{šËG°á4EdE@.VZ¤y#kâêíî_µú…•ð${ Kÿ !…0Pä‰é4ñÒ)Ortnܯ›/P'½˜ ZÅÎ$Û–ž¤:!yÆJW*<¼>þ¡°—˜EeúšFàâ~hOaú÷ä8D4B˜AǶ!¬þƒ2¢:©8¥®ðÿ‡2ñ5J2X>ÿ|ÅEJp5»† LdQáÅR_°»<;!¼Zl…,ýꉚ uÝ_rmñи~‚2úÔ­qv;©PD©† #¯ìã2ÙÝk%ŸFŽrÿC o®ºÏ‘FPÖÄž4ŠJñ‚H´Î~œMd×è ‡ ûS>* ¬1ÖS£€Ž‡ª©*OÈàë;Û$Nqª}üBaFÛ©­íÏq(A<äÚ¿ÃƒÒæ—¹bÙ©ó 0ÌÄ6qŸ©NY9›GàB0(¾º îÝG ’À°D=!,Þ*~¸`•Jµ×o“fËËlèù"YÂd"”¼>Z#Çéõ…_œ †EQ`-¬-0QîÐÑxàôoÔÍ£,®„9©•^¥od„8ª¾¶un¦øw3f•ÕA^A¦¹õhrÃegMme‹_O—¨˜1èç\–²\üÊøEa$qÈnjg`Ùц\OZ t°BÆ&„ФxÁŸw…S|îk¨$‰ˆ;³íᣫ+ËÙà†ç Ç@ÀSoWkØf- ÐÜaíè×/Á7'ÀSªjˆ!'H ’j”ŒAïÀÁBTú¡<[e ŽÛF í|°‡X²öm.>°¦@˜a~©b誦V‡I'È?rosfÒSŠ„,7ìzi‹ Mãm;…î‹?Öê²äQš_JYÔ‘ð s®Ýkzó“%‚`„#t™TÁ† Và“fÊ•¯böt[&b‡4ä&sBIíðXaÒ«€cŸÂi`! «4=ñ€®·½wQ†pâ*hïµkÕl¨ˆ ¥ŽÿYjãú†R`ºÚáÝk]&(QÙýqä ìç9ÄÆ¹ƒ;kâ§S‘¾¥€œ1 <0¼¿¸ÒÒÑs[£\ã”?Ïž†'\‹SΉ?(m³ê•O];ó÷š¦„I ·è&h$$`†¾ä%²ÆåvUO–'%X§™)-z&•§4ïs±Ìô) ¼;Vúçø¼^7MKôtÄZ¾îmdýìÄ‹¹å vÿ"<½ijv„1³qŠmY›œ2Þ–ˆêù1±Lé•?RŸîQô—k$xGùñÒ4ëÎùŽ£ì+˜MŒH:œJI‘Ûý«¥µò´K{TŠðÜäæ‚Î¥†ÉÍem2tØùÏIø43Š@;K‹¨†‹}ùGÛ¼j]e6×€–’º™—Öóû_Øòya&d9.$®yÏ ¤Ý^ì¾àñ®J F5ü¿Ù†£Ìl[eFôbeHY˜5â•´S\$ó¿1Çëñ®fš¡Dí 3HÐ".É|n¯ ëB ¾è˜~Š‘ ºb€ÐKðn@ùJ!rjŒ¤ ®áµå%܃¥ã¢³ˆ6' K`j=‹úŠDdÕF¡#ìP_ªÛ•ý~-5¤¿êeÚWÿ|fè@ÃíêΫ©}½Qöã©l%ÓÕÐÙÍý»þ¶C^ðÅê5\]ƒôÂÄ÷¹Î±¹Ì[ ókǺˆpCWPÅõ!§Ýœ‹ÔÚðœÐ$$/3Ÿ[³¤ö·–ÜJ½¦[ÙEbè™C‚oåÉ“ ‰á#Ìf# ¸†ú‹FTu ¸Bzâïuý-õ¥L=‹§#Ñ#5Ibò–œ'@|ÿU‘d…\iDÐû‚W}“m­Óí—a -é)¯º“J3“F8³,ÓHl·rFCk_~ 8p¸‚=Žš©cµˆ™HO Or"N˜Åܧa]ÛÉÝ }Ž@QãéÜ®Î$q_à9wj\1Ø5”ëkˆ¬q†€"D¾tdü4‹LHÿmêA>ö³—Ò<1KÄÆU¤%=n×JÌ„°d¨+´MPZSe1£°O02bÕ–—¯•=wn“kÎ ”qCÚVÕ!.~pb`ÖàQŽÂÕï7&/?æ{ªV0âñ ÙåWö—[ %tPDÂÞäï½Z „ŠÉ[Z}g¬,Ö0È$4¯ˆÒlðDq¿Rƒ ºuÛýüðì~l„Úœ.ŠÊd@Š¢P0&¤žT(Dæ ÛY'æ²’u©-¦l߯ڮ÷/iñ›líø‚è,»¥FØé¾.Ï3äº×ª®µ‚ç°ii9æV:¦VyØF ¹ È8^£Üϵ2°î;¤ö¢CºI³Éuþ¶ŽzpóY›í14ûçÖ´gåÓZ™>%¨Ö·Gör¶ÔyñpŽ´‹ˆmt£‹ñBPÛÖ&ØÅû¯O²/è¤+R7$ ðfQ4‚ЗÍ]í°“¾õB£Õ[Sôü™¬žu¬Kgσ à¿õÌàP*X+¹U¦­gd´)œªLSs|æå©†ÅárHÛÎ$þ)¦‹þúŽÛÅ;!vÖ›¹ó?­Ëýs¨Û¾)¹ëêëÉ·ÁaHþ rÿ&‘Ý0 ”‘¨¿uÑfô(§kÄo*˜SÀ/SEÏ>$a Sþ(ÒXý[u ëæxNÖ Vg’$Æ›¶î¾0Ëœ‹”ºŒ‰ÆŽdÌ®I'õb§´u`MXú>Ño)ó®ÀÄße¥†c?ϽЂú-p¼#T]ÐfÖr}5§µ2òk4²L±¨éŠ'/'FPnCzJnÞL2ÓÐëJ«—)-—Q¶˜‡tld„hsÛ‡…¢š;XÍ…¾„â’_±§u‚—º¦ü”ŽÃ€ù]yrnfi5 âÚoß›=ÅZbãmaûzidèpŠv»ïC"ÍIŽ›Î˜,ýÓûpžò– Dh!è¶tÑgü¸­ UE³”íª¹uxUˆJ€Ëíæ’2“ñ«,6)“äe¡™GšÀ£ZÒÃÝë<âÅ9Òj":J´°ùDCì·c)’° ?b—Éþb³(­ìfáßtŽiX!/›“ŽU²oü†Ÿ:ÚF_à™j?7Ë!¼þ%ÜüµçÂGo4:…C|‹ÉôÉuø^¨§!G.,¿~FÆ¡žÏG{ŠòÍŸd3ø©ÊßfÙ%;Z,:©=H.! š_Ò\úM¤KŠð»Ðû”眆,³w €×Wþ~¢RoÌ-*yàâ݇…Óн»t,¯~óIîÜN5ËwSì)‰ Ó]ª¤&‚¢Ü#qåÅ÷3uÜßô—ýd¬ßt0¶¿úËÈE:áK"(f”V]†>-øó«KΞ8ƒ?–÷æÐŒãjÂï1?¦|é©ôŸj‘õ?†iãJëÈý5vw÷½ÔªÀŠۦѴÁnDÔ^ü³<ßø J´Qceöl&ØD¯Û"€ì!Á*4¬þÍ/K­¯ÒªÓ’éCðmIÖf‚¸ŒÛÚ½Dñ—ÿ-l9m! ¾” òêÏÌTîZä>ÿp”Y°IÞÚsQ¥dNì×¹ôû Çæ§Ñ~±3“cñBö ¡ëݬÕ.‰íA™ˆ˜zjck¤ˆ´[­šë_1’'‘E+WMºµJ\ ÂÍg¦…R= kvJ§Ñ¬[0÷‰<ÅÐ9^¬·]ôá—;CÀ8»¸=â6 ÄÕžî? JL¥§¹n)¢ƒ‘5õÍAÕA¼5Ó‹ô€5íÿmœ²”}ç8CŒC²ßAVy±÷žX©ã(Txwö«–²Þ¥›ÛEW>®*ž(«·2rïžq3˜?ú¯ÁÛf`Þ±«³ ÜÁJ3&g:—þiaÌ ¹ãF/C@Sx.¢7ÊtHØÃ`ÜùD±úò™²Çeþ¸EIº×K@M¹ ߇€ôY±dû†í¡Ô‡‡©ãÙX€· ëhi4K0ªR¶b½·Œ ™‚ÁìÓö”0x˜ GaÆ5ŽÅ_—.þÒˆÀÓ$lÛŠª%† œ•¦’”(ñm«¬vDXÝÿ5§y'™²,Ò¹˜g<‡·S D³}V6ÀFÊruj®Ê‡Œ"¾=’¥BdJ¥’©rE›Ïb–w¸³ •5´øfë02e*’ +­úXu~5Ó "0ÞÕ¤?Ãd€êŸo¤*ŽŽw±üˆ´_õ‘ò°u£YziIuFëˆ;,[GQºõa¡è“±[ãj»U]°0=_@Æ•?O•Õ^fØ„¾y:b ”µ“é­AbÊb½÷! ÇáßÌ>Þéaö÷€øÜ´! ôÖ‘'Ô\6Ǻšý?mú¥ ½ Ù9åŽÂ+(†ù  ò Úy$-ÍÍ„½ðuïµGßÓ²iDxäÎzj¼w3o®»9öäÁY-áÁ¾èqÔ‡×a“Óæµ¤‚5 I ©ºF|Éž„é†PÙðŠ-‰á¿ü«dÇ$;‰ƒô³Ý'– ]n‡yI®95àiö÷šsKf ‡4¤Ý½dA¢ýuhá…H¹¸«×M‰–š¢øiÉÓ¥‚æ®ÐU®†ðèˆn‘…¥ Ô¸<c§¡Õ*ÿPË3®•B‚%î"¢:JÌ"É7j¼b¢ £„•°²7¬ mp6€2Ý'©Ã£½›O^•ƒ3–âBëMĪu˜Œþ ƒúÖSE0ã[9xÇË.pÀÀb7© ùÀQUaþ:Lª‘,?a‡F¡Ü½›ð3:Œ 9¹zH♳_Œk›5ÀÍî~îãôÄH-UB0ݵËgÃzãxPÄè_—ü×v§ê]?µ^ëOÅl‰¨F.5=t’‡#Öœ ̥ܾò |…ZûªÃöÕÛËMèÅS²?)õy¾YL‚{@æå-o… ¶#–ý£kز¢ü¼|­ZúÅÅ¿þ$ÜP µºÄ!@°ÎIÜ}ÉÏc¼Óâ»ñ¦¹ŠpÚ|¿“R¨dsÃuË n3¥c !<áçõ”pñKo0)®sŒ‡T:Ññnª)@‡Ö¢ºŸK1°™/+¤½ŽŒ®ƒ¯ÙKÁVÂg&ÄvÀŸtAƒ€š4-à½kü¶ÿ-ïÝæ›_@þ¤lèžõpÙ¥âÝw/‡tÈOÒ,Œ˜—–º¼³[€—ÿCá–ݲ"Ðà…7ŸÏ.*Kzò‹?¥çœÑ>p¡Íºá’öò¨ËwŸN˜ àõ¥vP£YŽ5zëËÁÞ]>_ë‰Þ7X×áÏ…FÁL?9ž)Qã5=qmÿ÷VÌ$gÛL©¸ÉÅ~é3/¸ë¥ Ò4cjqn"¡ çwAä} BòØØï¢ý¡TW9±òò3‰\³Ì&G ƒ†R;ë­Ü}7-?®ÿUðñ-ýlò¼Ï•µ8¾·¨³¨ç#@eôÈ6-Š#t¸ö›|î 6¬Ó,†üÒXqR»âbÁh‚Òà À©Ú±Ííדý:¤ù=ô¹Ápš„Õ?ñˆD»XŒš‹â/ÿ2J/u¯0€Fó òP¢•ô9BÅ ˜/ù1_]vgç+á9eV\EõÚâ$j˜p6«óY¹¾ŸùüÃEźvžLÄ}Z®?,¼gGmE¡ ìXú¢ìu¢C©ëü¤hGßà3®¸BbéaaÎaVÆÓ=þ RMðè ïï}lzý?ѹ°ü8T­Þ,\ mQÝdšòc©ü‚éH†nOCùæÐÀÏžk(?yÉŽÍ¢«dhj¼PŠ˜'ÕÙÃðC«úƒù‰JÅ¡!BG%!àbÐf ¯Lì¨úSæ°ù¨9°|þ«âÞCh‚õÇØÛA߈ Ф‡ø Ÿ¡§ ÅÎT匛´l¥˜:®×¶i:¡eè/óû6$gÎqkݾ#e†”Âzå›' ‹^vK4én®löºbX6kñ<ÈN A|-ÜyL%’œ”§ÏSÖï/ÿ†os× Ëwc]ßH1€æu´’+µÇY2ø£šbË4B$ ¹S¦i[Ô(49ÓáEPyvðå)°wÿ~ÖÏk¤ ²oœ* %ýgMìÉÙOêך•Z8væ €Ýã5R€!G>ôÁyŠ`~Ö_¡NxëºoKTÏħʖ¬ô€Ý^%n[½†“¿¼n’YeÓ*æZGæ¡pþ»¡ëOd@$˜Ìa-˜ÆÆë.D–Ö\{š˜"0àú™Ùiík™êOÎ0<r.æØ¿×óKÓW‰(ôNÒönÉ7Êš­rÑÇKS¶õ&; ºÍØUhFÔÌn¦J¤¡}Û ÍkçBÞåû{lªï?ú¼£l©w”eæ Qrô&®&™Å]Å= Ü›œ˜ |Ó±­Œ_žp©2ßmz(‚à”lÆ—ÏÇ&ƒá)ƒCF#ÝG¸> $uFÅÿ+€4Ú‹Nö¼u¼dy¡×b1[t1Â:ø‚žè’,ß­†S„ì‰;ôX? Q¯t)ê¾:Cdzj} ƤHÍŽçûi#Èú+I"üÇÚk~¯~VÊ SÒOËL† cÁ =8"k¿Kñ »©•÷»oYóf¸½mö3ûT)«»ƒ¥’¥v }¶/4«ìØÃÙ’AO¹‚ò ço*T18ÚåõÃÌ`õêP–8<mƒ˜K;n9…E…Øç¼1…w‡ÒÜà*ö”ÚÐ9 ëIÀÉE!´O¬JýkÎh<ë­ÿoé/Š1(/øOÑHSßfRÌ2E§Dï¦Ì«*Ut>XÁ|Š 311ù ·;rpb ©<ÿ-3a¨Í¿ÚÂm5”ªVj43q‘žÆ…‰Ì¸ ¦þoC<ÄW†ˆ¸ÉÛ6׉$FØ^0mî2ðI±½(dÇ,»1Ϫï‹üRQ[…‰bæÿ6çiÕDæÙ"·n-G¯AÂð˜tç?2p€.~áF &ïûµZb2 ¸ >…ÄyÜ#MkõÈ#×;š€ïµu'ûÏÚ´3=ÊæeV,4_ç¡QM‚¤sµ¢¡Î€¹.¿ŸBB›ïQ­*âÎøÏÎEœßGÖ”Ö JÜo»çŸq÷DÓïsJÊ’® }š£ÞÔÜ\xîŸÏ—ñÿcQ©…õ½°OăuúõP8½§' 1ýP}Ýýv¼®ˆXK¸_2k” (ï$Kƒ°xøÌ ¨zì2 ¸Vô{‘43ÿkèòoâ+ÅW«‚*]Øs_t[B9Ò%Ø’üUnç{× ;¿£° %±Šñ=½ÈCEBFe*µ cÃ<Цn4Å>Êb[2«‘¾t°”¹¯çe¾ eØ45ÄÌ!shDNºÕ‡5à`d4;ªÙŽÝ‚79>_mp<ŠÎ°èˆjõ·1‰F¢«4$“ö· TÓ—Ój¿H‘ì,e7Ws„@ÒDªØ—ÄŠÍé7‹Ã‚^õS‰âÄáVÖ GYÜåáEõ6ÊpÌ^»Çv¶¥Ü®2MûV¢ŸïaÄim6KÆânHÈ_ZÃ*I ñD¾W­Ê2£‹ƒC]âA‹lIéÇãI²ÊTµ^A¢¦@uG6„¨ÙOÂTö .€ü$˜£/’å9’õ´ZÆ5ÿ P(jÚŠaÌÛlÑÛ#G~³[•A¶2øØIÅU/¹ê8+¥z l馎ø=óÄØ­±E"¬KN†éWv“ñôeœ­Íë÷Sÿk”2nly€˜@ú&w ´š®öWsjOS èȽžGù'‡è„'». ) ˜,í’É-ßrPü$ ’ƒ3Ã˸¥ÿr LIã>aÚª!qpù2mÎU»dWœQu4ç'PÆknÄïó¿F¥÷ùtÑçjPÏns!U'ÊÞÒ³»×ÿ=TG¨!✑+tõÑ9šÎ×áæŽ¹rµÅ%°x•/4ÎÕ}æSñvyÏÏÖÖ?R¸ü=C€î‰Jc¸ÿ4WLàUaÜ‹]ïùYXTœã¶Üt#ž#àzcúY¶Å•»ž¡õñáÍdê0ãÆx–+}~r”Á&+¾Û—õõQé°Šg¡N9)ÈÝÙßm¾ýÚfP3H¬Z$ÄŸè¬Ít}4} #üD7°æíH%dñ7CÕ <è4¼yeÒ Ü¿âW>Úw|TïnÍ £éë?ÅýÎïÑô_‚% ”­Ì‡AK–'ÿ+Èz£Êq!“7Aë§€f¬¯úD­b[hg †3W…&æJl5ëV¸—ä|Æ®ÃÈè²|ÂÛqËálÄɲíSi ÀQ è䊠ÂÑ£kê?ŸÊ¶¸¯ðMÂ?4Ñsÿ%}à”-zmÏq϶W°°ÌõBrÓð`Ê‹AÚ¤0[MQU êw§'UÔy¦¼!L]fê ¨î¸Y­S­}¶6#Ëj8{$ÏôH-Ìß'âæDz0²Š’ð:œZ7¾`5ö‘  Ô ºÔ¶6Mi‡øxòõy‰y0ÚjÇãY“+ZƘ׵›ü–.¿€c¾÷–÷& ¤ä…ƒ G=¼ Ü—µaö¢I޲©îþL+pÂ’Ü·}®… ã’¢t ãéÊì'º‚Ô˜¦.íA0«µüRv$HJ ‹žbB…DoV щSBÈäŸ óñïFÍýÂ!d–$/#Ý€³ÔLm*N†z«_%]TЦyJ¶¨"žŸ¿Þá+wz· Ôÿj˜’ÞöRàhZôø\{§VeÐikj»7\ME£˜ͺq´…vª‰(UdG§ÖòO˜f(2T›BªÑ¦CQ#š† OWÆÊzU»ž™Æ ý©ä$h ZŒ¯gij- YENqàB?Ô;êšßU?²‘?xXrª£Ãd“êz¡ë²ïùÉI{5› Zî}»—š]žñ†¾Ší§ a—M·" i*¨²þ5C¨ÿ:yâáˆOSñ; ò«v³1TsÅZÛ2„Þß°Dÿ 4% ÎyY‡z,²"j"ÄþúˆNÆ¢Ôçä/ñœ®'C»ŽV\ϧÈåÛ'=AèÒLt~æ  *,ý'颅Õªu³Äxv-cŒš¹€ÍåÌ\Çù!d0£ÞC‡ÌÝø“ Õ»°ÖêQ>27Zå¦Å&‹ ÆQX‹P¾Þf Ñ$¬(²R¤¾õqRr>år~KfrxÚב–lYa‚R*‹H:ÿ>x¶+tŽ‘ºŠžyÓ€ûÎ=6´¸—t2iŽ_â¬í&ŠôqUÃwï¹#ÙÆ¾IG šü¹Ø9Gåø¼ãè_ëm4 jÐNÅ|¸Û¦–Ç㢾&ê(¬š×«ïi»ÿ#ª“æôÒŽØ»ó4RÁì Ýœ9“ÎPõ2x»ý̵ñérPl×´ÍRÌÐË7ª%Fç§xûö ûXo=üXN×ÙoâíOGC]²NÑ}hâv-·½˜4»v#Ü^©‡m³}‚XŒ»ìšé›KËM$ºB‡KkOZäÙ˜íí¿»ýðâ1¹$­t¬6wÔ$è-eÂÚí*(Üð³‹vm¸ª î‚ ÿ:M*8 |Öºõž¦L‰á‡~ dÈg{ºtüâ6¸LÙ"TèQqµ–—"!àþN%å@üI=Ì–ã€BÈÍ ix¤° Q“Ë, Õ|„ àøzX¥iÕûDŸeh 3&iÇ àmÁþI›ú xÄç>©#Ç¸Òø½(µDÒw¯®¶'MÚ©¸­=í¤Ô,·¿öá&˜ìí-³`³ƒhzÝNÙ4h[,²þmˆŸèÜÇ«‚ëL›£Èþ¤´ÜÞÔ]–Êœ ±T¯å9X¡¥õŸ3¼é)‘E­¿Ñ¶Z,(¨Ko d@5ȶƒÛ./˜Ô8 ûÚ døAÅOª8ˆ3AÒf“¦i.…ï #:ËN¶Ì–+´4Ý0=z£/Œ±'XWoÔ齇÷4µâ •â.gY;ÅU1Ú‹¿z±†Hp"  ™hÔÊ{ýÙéΙk„‡»¾$.¿ñ©p àåcN`ÕJÔ“ZüØÄ›ˆn²2çüú(‚L1oM?ݦ¡©oïÆÐ;@YûîDHì$m´:ÿŒ¼õÑ'óÙV«¦±téáÙÖ‡õ…dn ù>lÌåM­À×\íRÓámYç;  ½ïSšŠÝõ;S¶eBž‡“FR^ Ÿ÷qO_µç´T[È6%¹S¸õLÿw…C‡71‰]î‹Oü¶ÑêÄó ‰GCåÀ,?jÔ\Ú ˜OãIì¯BqƒÏAÆ0HDù‹üÁn ³X`V{ÉÒ!¬úYEUà·]ܬÌY Ï ƒ¾µ¤²Êjÿ$õËW4ó-Öl@Žtá0Ã!¢w”œŒ“ÚK&î$`j¯öB~>€ŒÂIM ÆX•dß­ŠT–;-hÞÀ©ìâ`jNd –¥ÂÛ¯ÙM·#•ÏJư®B|vй©RXæbm”Lw ý”VÖoHµå7¡çÒôÝ#ϧ[öÅuù½ÅàhU¡sçã“ U‘éù.rMT;eå$`‘z¸Ò¬îöþžEªö‡õÕj+ÕöhÐpŸMšã}yýÓ¸Hÿ'wgãyKó”©œ:Þ÷Ÿm7ýH=A›à'å¬âeRý…µ"úß]|ª,M»`ÔÍ&Ÿ±Â™kj£‹ø•^‹ªm·ÕD`ZЗ b¥Aì*´"‘G=зJ! Ìwq.ÊpRpî£,‚?¥F°"è6³-Í–Ë™› =+OÕà‘š_ }’·dû#¤ä’Gj˜_Ô›Þœhm{L2ã_ªÌ¼&s1¤°éT¦Ü]…t¶qMeQ¬0xJª ²ø§¡¢NQ*DÈ’?+2ßæE9ŠärXL‰„LºõÃkó\®É9ä@6‹Mî(‚H‘6Ày¹&Þ–Ù…Ü•a¯x ‘kÒJÈ!%z©ù²Ü¼q Ññ c©ò¼“¤!µº‚“ã¼öN€2!He¸ƒX"Ž’_áÎñ!ß§ˆZÊ`þþÇ–"ÉX$•Z&Õfs«;§¶Ý:hÄèäI…˜Àùî^ƒ±Ë÷Õ(³Y­áŸÿW-‰·Cõ€HÓ–»‡1«áã×\%ÙÜ(þÖ *½6wYþæÆmÉ™JKRÉîz qâÚÔlMÀ—ú tG˜!I_Hn?XÃj7d妪Z¸6žt8lœ*ZiÝ·ýùÔµlU¤f¹á6çÛQÓ„J~*œ[´F±üµ$i½6Åh6·: «Å Võ¸ý.k¤+AéAëÔ >{]6YI)áªæÁÐm·†§È¢ JNg6ú‹Çý”0~jbh7|Z…Ô–f2ÊëîÂüí›'b´óå8ÑZùéôª·ƒêuÜêMÃVî8‡à"Ð3R5§°xø,@x„V»ØœT3Å–Xys¦·¢kèÊ‘[ 2”ÊžùæŠÆ9„mL¸³†Ú>nFïÚ»¬¦F!ynkð‹TÛïlý…õ<33²Èa›œVvŠlÛ ¦:îcKÖ2‹–?8ÜÀéÎ0øßX ©Q »dx[^ê·(@¡µŽQÜÙí§ð…Ã=^ÑÔk]Q _—ˆvÖ”V–wR‡¿÷­màÏÖH‡H.òà°öˆ'U© ‰´Ý{çóT$‘ÔoœiÌÿ-å9¥Óö”\ךá}=?ê¤ l@]<¼+ž¥lz,-ß0>3P%.´í«Šj8'’õ0"ܘú÷‘ÿÚ*Ê€$Æ>uML`Q´ÊœÛî^±ûÍǹK… 'YÃmÒÓ‰/Ñ…½ñþÚ•J6tÛp%PÅc± (|TéËcª¼ù„ Áp¶îÇÎèzf#^ÄädL±ž0XÔÈŠ1U“¬ï¨nñðJ‹­€êäJzÀjcj¹;F`Ü3<_Ø&˾|þ°€…Oµ#Ö§ìýL–àZçQÁŽ4¡Û9LR_|H¢·W4¦‚92Œé:ØE½mçǵÆua§¨ÇTô‹§òjô›ŒÊþá"TFáZV7ñÑ/zD¯„…`î68‹9ûÇ»3U ùÐÐ(?º`Dg7Ì64”€ È$šÁ鮂.»H§±Ô´ŽŽØ,°W:ñ&G:þÖχÄï¡; Ê;sÝŠWå´2Xšº÷³Ø]ÙWSÌ´Y3hÕ Å\‰³@›Uz¾ÒS¢ƒˆsGW§Måß%!¤ýqT u}s0üºœ¥¿ã±Zˆ˜S⺫4”EÕ¼f@?°y À„}ÑV‹¶€V¡g¦c¥³"ƒÍœTÈÞ›÷àá[_æeàð­äAg©v}Þóõ u‚£ _¸–üj«ÿ)Ì0¡©XÖÀ+Õæd~£Bšo«)­=hõ>õœº'Ó™j=X]Nvž.ÉÌ~’1>|yw0¾%ÊäˆÇv-½¹å²°À3"{9ýTµ«Ë},/£VË›ºq¤]¿õ}ÓáÞ©^,êÇDÚmã9$a­r‹)8äÚ•"sU)K*Rxsߢv(óVyð» ŸÏ5U¨dLäú÷±»Å#ѨÅ!Mï!*›·\þÙîÔ;‚›eÁ«ÍÔHéõó*Ñ8$'uvbºÍI:«÷¤*ÑU¶Zõð¤:íx\¢9§kîú¨‚~ð×N%ÀÉÿ'/­§`s<µu|e”4Uõ…Ô§I⥲ÌÒPDvæ…ÜKÒ6ÒÀŒŒNì‘+m’îšI%FÄð ]ΦŠMp++2jâ*X‘1LÞ¦…2b%…§oòðhI§nf )”Þ·%oåöõ¯}½AßoOÿaÜþ¿‡D÷Ûеöö{ƒõt3ú·[õn·êì$"÷˜7?˜>çô/??‹ÍÃ*‚¬Å>&Œ©¼u·ŒK„JU[ðÝ3Ü%½CÚœ0ÑÁŠx´ùü-¾â^BWöðL ¼w€»ÿmN×¹æsSÉ×3¼(–o¬±ß±™Á˜£Ô‚Äð•ÒOrŸ¡Sö³Õ°ÿ-z¼×J×Ú#•þ© úÃ5FÇl-çæ"é€ZkpלàêxV:EpIÎOBé`ñ÷•3D‹,ÂÊoCr±ØæXmÎt¿òò¿2Qª ŠÀÎrHN× X(,! ¸¬&ŽBÖŠ‚ñ_®¢7Å‚øœð%`!òJ¢yñEÁ&D×ñD‘æt{K±ãm ¿Gu)¢ ËnO‚2Î(ÇÀ5•7©Ôú>¿7y t™þY̬0<¾o@šý+!|µ;¡ Œù W“oSa‰nz€îõ~Á £·GO—ܹã·-`ÖNÍ‘¬^ƒ7mߌ)ÐÊU5=ÍŠÔJfÖ…cp½4±› ê<ÔÜþuç•™†kRmç Äá-OXáEN¨·%^ÚMu0Uì@¶^ÄÙLÀ—ú®I¶Ç{H÷v <8™>®¿« oë‚Ä–‘’š·˜êÒJJìÏgtÞ_áãè4®AMj§X ïh÷å 6ªÿ0S[ž/Û §:l©ÅK¬Øná‘Y {ª*oq²iEwéaõòG—Jµê·s T…Å‘È1§'Àøªõ—5&æøš{üðfd>?¶êò©kÌN§úŒ‡.¿ÈøŸÿ ƒã…!Bè#êš1—PØý‘Îê}ìÅbx¦ÂЀh°Ø%%éL.ÛZjà2‰ú+æ+ 2ªŸc*~в<{qí¨w´M«,éï?Ñràÿ WÐaÉŠ½ó2ZÉä•è¹uØ•}ÝY=/Yé¸6›è4”É\—ú^å®´ “ÎÐj!Ó/«‚Ø—AþC‡hºÉ]€+cfÚ^ê^©)á ¹J˜ZKŠqžèùà¢=¶( ÈÂñ/’¨ÆË‚Î=¼OÈ ­-}mHNØAyqS%wá!·®QûTÈ9Òg6ÎÛ3˜þ&SFEyöÅPÒ;rBØ{bX&E%xešÎD>Åã0µÆ¶è'(j_ÔèÁ¾–»HÓw[B_ÕÍáŽJ£' ¯¾ŽCh“ 3”ÉØvÑiô¡ŸiÚ§²þR$ƒïÁ¼ÞE¢&@çú‰=`”™§NšŸO³þJÉ¡æ>º!LÁü9õÓ8wÕÌýÚø&‚$ò_ã‰Þ??v+‘µî§C.ômC&¬Ê£Ig&Ù dÊ¿g/ç¬ýM€õ ›ÚÓ@ô.¥M“°p¥µïвf=uHº%jw kNF:TIq•4K®œkK;wµ5VÅ~¹\“¶¬ËªpíiðMŠ{hnýàÏ«L BŸ¹2)t´O fW‰UWåHª78\DœƒÝ©Ó÷ @x¿‰†²ö=Hü÷wÿznõ 4^Œ¾òܘ+ôf$hU³’œL2KÏ®üØU$’Å -Br [SÕ9™““nupV‹#¹WM*ÃøÚ>/eöæM™'‰‘Û苾ĪÒXö#o°³Ü 4l1j”l©)™~«‘G3«3QM¾Ð»poáC6Ç;tuM_î⃻m“ͱ¹ßpÞ¨¡Rû9H­àÝw7ƒ—»¶¢IkðKçàcPœîÁLèN4Nç$ý.•Aw/Îäwœü6´Jzਔ=¯8£Òûä”ñßé¼Ê“ê{=1sC[1› ™çô{,$ä°ÊឯÄï÷ ú+ b›Í梈ýë>ðŽö5 èV(ÝiìL â£PðN®còU¨$¶1ðßF E¸…@Z*8,05|uää õÚRè|Ojvs¸ú.­¼>!‘`v(óâK…H»_ïÒíCêí"¾ZàéNM01‚.¾„ÊôInüÔ:ÄèOVw/*‹éÞ>x^ ÚU l£?Ö.Iu Éð»Ï>êÚ[\Ž_ÿµGêq…ÑÝ2‚' x)?wf1*4(²³ÖnK ½×t¼ çÑpjýî­ ·ñØSU$~W.¢•ë}T-Ç‘¦à°,P`WT:ÆLÄ2žù£è= DQ뜞òVÉK2~.ªÿ:i[AÿÇjë 6Ì|#Š#— ŸÑüÇ@Ôœÿ&YG%шi)_ù{Jkú¼‡@eŽ2O?ùâÂÇëG:aÎé@;Ü"ýuÿbf‚­šÄf²Æ- ¼mŽ”çØ,÷K>t)Íæörúͺœê´þLÂ|þ;–|ÒnE]†:¿@5ÊòÌ÷tþG*䄽¤)Í(HÆ>˜‡À÷V‚À±#+2ΦÅßg­÷JÔ[¿Óˆ©d¼-i¤X -šˆ^50<¸ |ñ–Ob\Ìòî§>WRéTÊñ=ZQŒÄ Ò'qƒµmntK«ù?²œì»øƒ·N—ðö:‚÷r å÷Cámê&¶kGÄKñ²¦BÄÅ·}ÃÎ@kLBYjÏ’ ¥˜SlD#Ô`þ84í9Í‹7F†9kÁ©H‚ð­mŽ|rL¶¬O!¼HıÉ4§æ”?ÞSÅ3÷ÍN_w™ K\U5Âõcݬ<„–Pq ›)6‡¯ó¹õ‰ÒÜÕ$À!JÒ¤0¼nÙ¾>s |>ëhj_ë"³ö[Î3(×ýõeÙª?9SDf¥.(Sg¨ã̹ªî­Ë"›¸d=O:Kk-#Ï’wì¡(+ün©Å„__,1àýÉXx¸{„‰?FYú¢*×UÈO™ûà·ç`æ•m3)§u…“zÂsøß¬è+ÜCv—Á¸‘Ìôê¡&]þ1G”ø£ûùQ,²†ŒZ¹{)EÉSË2¡­õÑèýH“M$¢«ßs €j]ÐvÚ”òSLHhi| ýëÓŒìe¤ÓÔq¿ßê íó»;i‘Lçżr Ú¨†oøó†«|ÿu¬M¼qñ˜#¤(ÁV=t»zÕ±±ï&S¬¶Ì‰ðoIÚ[YÊ™|å”å§ÃçY¸â¹³‹¼[BØ‹åGÏü‘†J€ h-«Ÿ{~9Þíj4mck1.d‘ŒIJ¯rS‡Y*«Ã­®z„NÜhGR ¿@ÄÅ5Fø±šC'P‡²0_"D_N "ûhžøê:í–®vµI‡I•FããÑ4kP#ÃÚiAŠ>}^[Ÿˆðº´ž‚ÒãNÎKì’nñ™ÿçUˈÔ{’Hã]¢ÛRNS É$’I$ƒzI$’I'>‰Yd‰H,Ó S3÷š÷ˆmšl•h1ŒOåV޾콧~ç6I—L±ìmÛÈ,~è½FùŽ˜ÄÙ ‚£4¨“}ÑIØ(›M wQÓÿu[Þ\Ó>É=µ9]è5P 7^Ï}oñõ7œñ8Ñ‹DÕÏÌkHôRQKõž 5 !Ç‚1'™~ûYçÿQÔe¢8ž´ÞñàË×W®«P9QU¹Ã(m#Fy‡ÉDBÑ.Íäói£Æf÷A/,"|} 1YꩦôÝf/×dlf-<® °œëu·¬Â¸|zQÍ„y¤Šu›çYÈò/‡²%ÇcügF5zšn*ߊF9:4ñK›QsEáÐz´_*­7VÂ4c€s2í†ô¥tEE•$gR•Å“É&ôQI óO?'›ž’ƒ>—‘Þ™Öõu¾dõ£Í*cß1©âA:í±†àädGPÜo¶ÎO`,8Ÿ°°ô·f¸D™2kÁ„xÎ2y&hÄYH_L»Làš¤D«ÔpEÆvu®“,€žñ=½îOMûüP< )j4€û¯îýÐ(+jà¿zÅ3FÅ(‘s¨4(nmãCó]Ù’ »þϬ6dIBå†PP‡‡GJɳÈê-Cœ x­vQºwcBŸ ¼K¹Ö¥£¹”dmþê'®vx&mšFÿm°1Íðíé7ëÇ$dàeèóqqUd 2OÞ+¶ª¾\zÚ+gdvÝ È™œtË¡îÉ_² FÛ’I$’II(Húžª®º7Õ2ÂÑ>¯Ri÷ÏSf´:3"bolpD(ìLÁÿuØèÛ§¸ä¸û@îì/+K‡1õàVC7úÄ·•¢|žR·Ìí@º¹š+õšÈ3Žäó Ið"øp®Æ|ó£Í ]hF*í€d#_@c›¹"ù@¾±_'Î7f;Þê%½¸ÅŒêÄÏČՅj®¿"ì·þá6FB}±¢Ø5‡ÛQk2¡¼Ž…ÂÁø0±j†[’*'Ý · ² +h1r¬=R(¡L±¡í‚äõLq×Áž éÏü.%B÷\%¯*›L ûðøÚfÒfj †^¨¯·snF4m‰ñ˹fBXÞƒiˆÂà9´õQÝà0YQö×]¦þ×|›šî{*4ê4QІþØÕñ:#ì@ÈÝq@dsÉ%=¯ùáRËÍ8#x@ˆEª‚þzÓµÏb}F.µËJ­-Mž- Q…¶öðý¦3]Ÿ²M|Sæ–|F—Ë«éÿ}8þ1œ6Ì]Øç“¹E,íRæFÍ¢r(¤R«1lƒM¿'Þ_[0í8ßï ^ζ<2xÆÑ‡LK$êzf|×VÂ*‚C‰Ñá+(ð¤¹ÛØ´ÆçµígüSdñ__æý2|  "98×Eòs_­y£íaù…P 1ÒÅ ž}º%ŒcÄY3PŸ¶~ ¹`Ñ<Á m¬÷"}d¼R˜ë¹CÊ9«gDÖ²lYm€Ÿ a˜§ÊhïÔɃß"H’(Ÿ~ÙŸe·VÎ4Û@¡û²ò ðÁ2#`YÀ_s"ôª‘Fã„7ìÆåmH“Y°pBT¢©Ðdºfãñé(žÔûX,i¥DÉ÷ªTöjÞèÍpi•·‚À_áÃàzM}²îW–wBbWpsá&Øû +H|‰ß*ýyýmÌÿónÍ܇ü'ýœzytJÐ./…¾‡ŠøÖ»,jIa^käC˜w±gÀ§h­“Fñ¾ÚŸk =L²P­'€¸=L} *]cVW›þƒ¡U†Ê×l§45˜…¤O®ÉÉ«\Eÿh®MFÍF[Fñ9-8~&œïà¿Àg©é¹nï`s1\,*FŒ¨LÔz¢Ý‹!§¹XäËÔ7¸G`~DHw}:$|UI#&ùˆK•ièÁ‰O:¿ŸØ ªÙÝ„Êk,oŒ š#ËÎá…§u4òê± bú2éצì²Lí… Š¢w1ù2Hcyé¯<_ðËÁ­^±Éü–AÁöNLéÀäîZóιz¹é;ÂÜGŸ›Ëmúú+$ T '1VL½2(fc§—+»¯%e­i!²ŽL혴–jB¶ç{”ª¿™c" í õa²lì:†¯-,Þ§Ì9‹¨@P3¾PhšýÒÏeÉ›ëƒ. ¢/!1¨O¶`¥Ì¢Äó Êý¤?²f³éï pj>Ëš4K"1Wž°¼/;Z;_èyÅD‡dL†™w:sëJr€d¡´Ùzï™ÙQ©@î+âúoFçØžÚÖMÜ3âl)ˆ—$·Dé©a¡à¼ë¢)dtލ6Vǧ¤ “ æŒGêî’Vê^áÈ?±Nãðê#Aˆ6SfÇ_ÏsßÈedA´š*$ã!›L5z}o °Épµ~1`˜§MGHís|ê{$òô”fb !Ò#îT@ñæ+ì¸þ§ìBšÅ¢ÜÜ—7Ðôàö‡Žuôm~XwË=ÛÉ0µQ;%Ôëp‰ÅŒcÚ+Eú4õo@œïöH-X.†ZÃŇ”ܽJ$u{Úæ3:´zERšº44¤£ü…Áå嘖ž²=¤òô_Z¥®YQäNW¶VÚ§”»h1ºAµuQ L £t-[÷µÐÛ£Â-Î<µ%XiLFˆ­ØÓÌžx6– o¨m¢RžŒ8Ø›KÆî+4ôtyûà5L“Îö¥öÈN|A0g3qÍI:+.5//{Ý f×tþºÂ1‡,; Á&>h»ô\Þ1™érĺã“íÓÇW¦Ç¢4åú 二-ÍšÑÑüéòù5}lg¶‰4„#Á›»•…9:ß&¥T‰ìØŒú#ÙŠ |Y”¯©òrGD>ånžD¨ü}bþ æ0eÃx<ŒÓ^بIVæ•™»A3îé@¶;K0dÿ;kµÌš;ê¨z`Ú!jšh,£Ï’êˆS\:%åj°Œw3! D¦èìÕEW—ñ¸^À_•<jÜÈÅ,ÇM¹åñbh⩆©¸š¾Ç¡3Î pö_Ô¾q¯ç)(©ÞŠWº¡ËK»30«Ì„Åõ;®áBo .¼/Ògs=셼ŕVu°WæÌ4ÖH`4ñ듎2ÌÆÈw¸ÔpÕi3Uî6)þ·~eòW¶Ò@AØzÔ¼9W—ùkW„بÆÑ ±dnó¨Qì¦DžGÔr.%˜ZKõ"é\ ÉŽ3ïhˆíX[¡Ô8|h«ÝïJWiÕýqv ñ!QÊFܳ’|뇼5èFhã/ÇS|‰¸l;Èzwú7ڧӔ೟FÍ™Sè‘Ûù‰6 }vlŽ’¶ZškóG`f]ÿtÀÝ&pdÀòÈJ³çò »§KÂ4p£Ê0‰[eºØ¬.G¦q¿ú•ßL_"cLÁÞ*_þ @bׂÐè9µ1~°7k9Vb¹þ8¸aýRÑŽQÕ3{¹¥3ÍЊK$˜cWá°ŠeJFFŽÅ€gsð,3¶cHy‰wÄyÉûxq¶:„¸Ä6aê€Ñy·ŽaÏ£ø“þ£Ã0s5ØœÏQYæä°ÛKóG©Éax' Švr”Ïê?YÑk[{0ý^ð òj¼%Ÿ" ×ÇIÿL "7ñ}½9Ö&Ä©p8‡JC'-+„2Y[¿$ ò™Æ%Q×áëjŠÄlg™÷U¬©¿IŒ¼\øÞä3µ‰Ž_m ÞA‘ÿƒ³dgˆb )™ùh¸6—ßÌjưêoÁPSÂ(Rm«@çóähhVì·¨ýÑmØØ³·ž8ÖrÏÔc'«‘vôÿ*áéU¶’¨þ—¾iöo®IÎèsãlUlpÓDmj²š@:È:¼Àì[UãsË+.͇=KD”Kª<Æ>€sÛ`mŒû…¿³Fk€T·È VqÊÙQ‡Ž<²µËG˜Ù¦äÙ]Ç æãÚ›7¡úðX,ŸÚ ç °vŸÝþhîÏëô1e–!¾¯@¶þê†åõ_§¤Á^UôéôvEèé‰×çõ•Îá옂Ƌ8F:¨»ÓÇÈ!v>¹¦f/g„Àîë@ÝtÍ×,äæéiÜÐvd3«˜h²Š=`mt¦ï MLŽ múÍ»V»­ñ¢u“ÜU2Òäe¶UÂQÄza™(¹³ jEÝNoÛW±\¬üÒ€/ΗÔ|Fú2N|£¼ÐD¢\?Îüs•¿i-P§ýà$¹ü^['~àЪ‚FíÁkê ¦y(Íø‰H­­±¤Œ‡ðyÀûR¡…ô<åÿ X+‡µW¥Ô“°áŸk½q‚8ð7·ÖÐæµörí€à¡†Ð«0qö什ÏßúëUŽÇܨ¯›zYóœ´j–¾ŸOÄdôK¨µl@QºØ"„Aåñ‰‚mæÍS.ÏiÙ¨CeRLÈÊ*ÜybPK–?sZ¢ûlŸ³::_}çþ „›è®Ï¯$Efö ·…Š, ±ÝñkšérÉ80¤µŒ‡BWÇ|)ßb;ˆ~™B˜j®¬"(ÕêI ÈýB½Z:¬Ì®à2hw#bC`¬AÅa&yýÿP°â¼o9ÏÛ0Ó…:¨MÖ$óá¨ÖëiËÄ'>–íCdJ¢æµì`G‹‘÷t«×]—ý…ðsÈéÄò¸ÁVWŠèê,ü%Är©àÒiÊqrî—:{ì ®Ó ©T&g¯ÆQÁürã\˜aZÞ@ תÃ(*ã va^2 ªÄ³2‡ýèdˆ—Y„GËÒ9›NpØHµÜLbCß>±r_ZDYÈH§Q [¸ç·RIÉ9 O—‡^)˜Äå¥^\`TuÜÚ·KuˆêPK1q²¼+¬Éš¢¨ð Šmò& èbÓU*»›£DàÜyæAô÷Ñ'rÃQ]b2ƒ,‚/ZÄt$R´°`F_q{œ¼§ñ!¤°!×'a%ùžûù„Z…Fp¸: ³Ùïx&F¡‡{êp=(iLÌX»(úîŽ1l“å û#µJOükUù]„áû€@÷ 5Ðýp54ÁIX5ÈÞö³§58Ý~VU7´ ôGs×Ðêeã9TöU{œ[²’_W)n#ê_:û?Et2(E˜$œG(©–Ò»¼GN…–éŸCX,4+ÒCßUÊþ"Iñ Ë“V(±`|¨‰¨Øz5•ÝMÑç³È€·”§îïædUÆùj¤ß3ÑÏ)¡Â3¥O#IÝë…Ÿ“D{lýÐs?/¥àÅ¥ÞéQßæ¼ pÞÿN°³Ä§ƒcÑR`½áú¶ùû#e~®•°ý[%ú¶V>Ãý]à¾~Ç_?hcáìÿjéDˆ‘ã,?ÈhS|OƲ8qÖâÆç¦ 3)‘#ÉÕ";à¢\Ñ :×wÔËÄA0-­¶ƒ¬^;8àQ­ó‰uÒx"}ªàñ ÷€ß‰ô©#G¥á.›ë7ªŽ$dŸTïÝŸ^ư]+–F4woú\¼ h=@ßI)Ò¶>Iú/!°CÈËÒz;˪_©ì¡y›ã„ýeU6e×_ˆ@aC ÜáãÅfÁ‚¿(ñÁ[œ¼uŸ qüiØQö¼\=G^I}þ‘âá]fj»\˜pœmâ@èÜâ󆺧Ë®&°®.¶HË‚Ùö“·¤­Ä4dóéÊfµÆQŠe"; uH2>Ó,Dï¡Ï«Ð‹N)Ù@Ó,%B"d7ãóT¡©]È÷®Ê[ájË(ã}ƒä@,Ó/+ sp .ó1ÍÉŠºjCT…šf1}M¢ôϼ8Õ/“‰Œÿn±‚7.©·)þä+´Æµ1¬qŽV‰©Ê1ˆf¨†Ü'Ô{ËäRóÊæ=œ]³¦„ë;Á®q¦#xŸr©¢3­êþxö†ðó…ðî»Ö£Èàén“4E`?o²••¶‹~g9ôi@°"ç‹ ØÁ¶n™`£…ŸUĶE›´é&ÎЦ8ÀŠ…æìùT¹%ülÐÒ18ˆµ¥¿m.‰,œ„]9“©‡%MÎS ˜T¸awç;»ÑšBM^2/ò£KíúIå…\pPâ( fdiW‘Åôþ?ÆžüeÎ']ì`®ç¡ŠEi9¼fŠ< –lÚº=œ,GÜØ@@,ËG'ºT±Æ‰pzŒþxIn÷(>øUÇÁ¦YUâ…¶•æŠõ<†ô°æG~çR£ŽÌ`“Æ’)Ôΰ¶M‰ ®™¯üí‡?xuEL£³Tø ª÷agþçDLrYØ[¤!)óþéïêÍ©ùÛܾb7>jÎîÇv„ÃÆKò¼¡m4?½ï\UÿrηÐa[á­psDçhOòÖ&lÑOŸì=>·iÍÒ9eäbÿyÀMUM(Î÷Žtékܸÿ%Ñ.C ˆ%ŠdFQ®½DpSQ Ñ/FîNrpµª9I˜Yåu|Õ}ŒŠCª$²ûÅo8P182‚íj a'nþE=½eá|ʤ‚N|µi ΂&„>z¥r <›=¨c<%ÿY$'Ë<ÛCBî/ôÊè ƒ×ÀAÀˆ9n}g‹­càFûȹ„ˆpŠ È ®@±W# ×W¹)ÔG¾Ázèa»¿B:|yñ§8vy«]Ò¯ô0ƒì]¡&Ã%4EŸš<—þ!‰›ÙmŒk(–¦:i¢»×,ÎÑ,oxùŒ:µÛa›¦Üz æÀ×)r;[-´^c9ªßaºfÿ_¥!}H„•Ó‡]—U¥M팲ùØÿ,Afû•„Ž_s© HWó9îmG¢ÕúÖÝ™‘LêYA²º§l0>çoÍtžºÚ6(›?¼œNuñK›.•þ—1^k%:‹v%Ìv(s׸Ìâ,ʘÑClf<;TQ9ŽT§DTÑ«|÷SWÔç“x€ ®ãÑÌGÙ£liQàÒË[7µž:ÉÅàdpøÞÜ9-±}xÚ5Ë÷f`%BÜlÐò_…£8ÖJ²¸Þ.KÆý3DH›¤ ÑmJ’ôã_GÞ¶Méš,L4é‚&Cǰ ¨ ¸ò£0{Ž(©(b̘ت8ë/¨ô‹î\´ñ¸? ˆ\ˆ#Έ©ð§êœh$†…ä‡Lƒ¡^ÛGOÎtädÚܱÍ_å¤èOÝ4Šò>k÷²¡¥ f,Ôó=îPÒŠªä+¸Z­à‚6¢@-k)Z£LÑ–ø20EÝû<42PϹÔâÁ„}0õLäp~~æZ?FÁäjPoEŽísoxÇBÈÔæ|7q-Áâð\þ±H‰…Ü öµ2*9*ždEÃ3¦CÏ,i½ØB’¼—|Z_5.é? H’ÐÖZh1‚·/|"”–? LðÓÖ÷ûûðú‡ƒ2ã->Ùn¬xB>Á ÿ1ŽWú  óýÙ9p“¯(…Xšÿ8Z ÁR …!Ö9Jö€ï,ƾoA‰náÒJô´|E/ 4rÇÌK/é`Y³+¬Å r¢7óû[´¦=…ñ1æ´<ÊtoÍ‹87ÉB¦à¾ñÚ»ir 0«K/k W9ÑÕQq7ùv££ùnYÕ¤Ìo„Ö<Å•„Ê,“,þvKAªbà†ñ3Û—(ã¨cäê„dÏ•>Êb]­³!M2âÄ6ÑÏ*:Àãúó`sç'áá‡LzÒº@“ñÇÚ¡ê©Ù°%Sü?«»ø„†«6&8ã¢à­¿+g!†{–×e}ã)Ay œq|Q–„±„:†/q!Q¯eoƒt®k¨ˆ˜$c->M”åFI¡Kš@:ŸÏ £T¡Qr'$ RÑñqüù³’|Ø,°üx0ÖŽ±î$»ùÈèWÞ”ñ: þ%[ yDé"¥Hr& D1§Èd’L]AûÐßñlˆ+Å_mÃ’AK[ù®¯}XÏGn~çS¾!ð6Ú™ÔPäÜ`Á5.$3)<›Þõãõ~4Y*»?‰òxŠ™ÝDb ¦Ã^ôuãiÔqÔ“º*jé,øõyr£±~Ö¹r°Š§sOÔ}X[½ÚQ¯#q3êÑêŽß†—tÝ;Fx¹ý'Äv߯÷KR.èy˜¶¨€Ïâ–Üùα̲¡ô‘x1eÈ“ûã’çœèà\Ø^S¸=÷‡:á‹{2)·Œ)RÏSv„Ûb! 2”·ÏêþázÙ3…cYkèü )Áw1»UE“ñ,æx˜tî<—õö_\ÁPm¨„ÈWijœ°Y筮ˬ¹–£ígw#Êô9 ™6®ç³E‡ïäWy@6}et6¨ÈARÚÔB~ÚGÊl° PÀ¼ËgNqy®jcØ¿‘fi§B˪GÁ2ÄÿvD¿½—wé¾R ÝûVí-| “ìÂÁ,‘/wõ죣3þbs$£ ¨¬Fóµù²ú¾b'îªÛFQE¨ûòøÏ  òâ¯×ÞÕï®q]so›*ü-7ƒ]ÓkÑ“tᨘJ|lïõý¢X )±?£èò}.Ý]÷± æmà„Ðá¡Hÿ(eÐÒÙS#¤ÔÁ,e-¹í|¸•½ÜÚ|_›Y²è%»Ù4€Rû’d=ÀEèrÀBøW³w¤´åw”l³šô)K¬ ÖH.ª< ˆ/'ÔÞýnê~YÒ“u¶X gÐ|…3ý(ø0B9“yFkÉ·Ç-·ñDÈÉ#®2ýN×zFäñD@u¬Ê ù1Î[÷e|Á?åvôšû¶ w%ƒ1è$ö ÁÍj·”øÃ¶:-Âhb<Òv_O„›:œt>ˆœ´©]!Ò­&XÉê¢ÓÆ\ÜjÆTÔÏ™áU3ÖʱÆ_2,ÛoÛÁvª‰0k>ês}À£÷¥ƒVÝ{[ƒ×³x¼•)o¡ ’¦X‘%æó›Å(fÿ:\¤¨¢x}¿Çw™$¡‡· ‚hàÐÈh †ku«0ò§ñ Wøæ‰Z—[ßùTÙDÝõ„3ˆ¢¼¾×whŠÙåÇ¥.oò¾GäDˆ…s»<è9oŽ”=Ä|¦Fˆ9 ˜§É¼sB!¬£ÍIZ€´ÝWÁV uD Î’ŸÇÇÍÚ­l@Ö¯q‘ KåÍ`ú=†l€)‰»pj|ÈÕ«Ó~ØPÙxÛ7”I²ÝˆÒ½r˜)Çb9Ö¿]镨>ÛtêÌäù‚[ØsÓòh`KzK>´—xçÕp+4&·_:`>Ëg92DçyÀY‰µù){&»ûɲY×ôòeøðV«/-ôÇxY¢ ö 7ÿL‚Ùd\—z€k‰3¯–ÝK¬Ãö­æ?’×øy¸^ Ï `$H¹Bi††þªŒ(©ºA=ÕÖŠXpåÌŠÕ¸ça0ã7~ÓxŒHùôÊШ’‘0'¹Ñf¼ÉÁ}¿‚R²"몇ÞÉ”íj} õbDrÎtî Šˆ*IÜŸ\å)vÂ@ádq#š?ÀCį¼mH)à1'à®ñÿ(Úv˜kIŸ‰ÿ}jI¸¹a;]Xkâ‡Tü[1# YÓR›ƒ'Lç÷ñ:tïk’*"Ë—•ÅI @ÏW–"þ<ÇO²šø°Þ„ êë3ö¶ËÈÏ¢ÏNY¥Ã-ʹ™yí¾;“IbÄ·¹œJ 1ñžc…•èYað8´AÅ¿ÔqÕÿ5Gcì“2ƒRp#7ð´F†$}° ®J(¬Æ[eHÈÑ~ƾ}F¯ŸƒI2µ§Ðb©Ý{€¶Aäe¾Eðì¯ ›:?Ô„Ê5^%X`ë+ñiç{VL›»¶níJˆ›F µV~±ð0çÑB­êOmn;¯Ã(³V oö`ý¹rôÿ-gÉn¯þ¸Éu£Û4‰7þF‡ÖsS¸![ nÐ>/Ôy¨—èYœ^teS¤L¨Kñ=×çlÒÇñÍ‘‹.Ù0++ÝÀtx5иÔSwÔÈêç®î7q;NbÞù؆*¡ù'œÏLòu(G†ñ¿Mðô/KÕ¹SJ´ÁV©3—ßÓéì§\Dðc`Zžªî‡dðùÏû;ø[ "ìù$íÑ"è‰ã€·´CÆàW›¡"ºrnG‹ –)8H-ënú! C›C+¦…¶[>èÚº _fù 7 Å!ï$ØÑn†„-ä2g½ÚMoï¦þ{I¯^9Ò~bz_ÄžOµy‰éXëgPòÒý\ó (·žeÓ>qâ–¶ÓãÁË«E¸pæ´ùìæç”½?ÀAO/çžoû®ÇU2«)±Æòj DäˈÆ/súË™àÀ[Õñaz–[t!õß I¯Û)ÛÎÔ´D|,+pXÜk!²÷•2ÝîM! S£ã•õº%©^ UXÞ°±ŸÒ™)ùކ½{¡ß efXô¢Ûk¤o—¼©§l+Jd0< Š,©ùÀÈ…°l-ÔbO‚7Ø„EæË†;FÓ9ÿcѬ$ÎDÄ¥3y‰#E(~Á9ˆö0÷†À(Y;«äÌ›éM³ÈNI+ 1¦»l±c+çsóÇÌ{isàx7ò§ºÂö!ÖùÄ7Âþâqv~é¾Yé´žŒªð1ºBÅÅz“x48,9ÙEY±î¬ÜÁ¯ ç“îPl‘Ü58ÇÖÛǯ!m™ì$L4Ç[F‘m<.î£éñ.P <‹â%yô7çdG8¾=Eh‰D¢ãþ…[!§óZP›jmýIóe`jmRÞéЈU5‰œëbmØÔ¼¥ ð–ñ¤!È"ËEg[ÆCTQgòÙäØˆ"¬}Gz/À…´SàšÌ³­ì‡4½»¼ã™Å / ¾.ÜDülU½9g~',GÓ’¨'…C5zD(­7­K8”Ù‘§}š§è.Ηd Ò¶¿„tñ%¥sI7ë#މÆN'kšðÖÜØðYKBŽŠÙjP½¦S«±ÜÏ;5,X¡Êææ/„6,$X‰;¸Øÿ=÷ƒ‚Û4ºŒ¬IÆzxÅÛTåŒ5:RñÅVn¡Hmȸ²mœOå{Îð°¢J¼¤Ÿ/ÿ}÷R¥ˆ¿ôk}I_lY‚¥ç¬G¥q¶M¦æë1ûõæcbßd`’ù=€ÈŽJ£ pYÇXFY¼ì 7¨6¤üÐÏ-"Ù›ÜC+D‡Õ¸ñmRX•&Èä\nÓò8Ír÷ò G,ÉKuI_S(ª¤€ïf¯vJìFõf•o„-ÿ4Èš@ðÍ¡!‡XÌ/Ó GæõÁhµ.¤¶eçç‹JS»D×Jf_Æ^ Lrñ©u=×uó$ø0ðŸK#N>©˜m°àrw{8z94.Ö¿CŽn*ÏVL2žÉýèÕŸ’ÑD¤¥|÷lx¹oµfAÇã±03¹ Ä>cLf#E3g®Ú0³jóæ½ºo¤«=™ÒŒÚ1—ô í_&ö¼Ò³0(x{\±…öžòôöJoueçüÖ6ËgÑ0¾v²ßöSlM4L× ¤/_`4$¢VÙÀ+bÅ 8gÛãöô}[mûzý½–cöôc}[Iûzý½áûz.¾­ý½~ÞÈ€ñbæ´Ç;ˆìNÃ…{ŒÞ%8¨çqô)³[^EÓÍßí(°¥äɲ{h……-"ñE‚‘™n'—Öÿú£à÷úÍCé-~á»gø©°ÍvÃ÷—B[) v~Úm ᬼÈxï²Nž-Á©-M˜™(ĺm¤†&À£H(—ásÒ`]+?†œ…bEpBÛTŒ,Jn˜îMÂËÖ²aÇqóΟ#õ‘£­¥Õ³'<<í€Ð¯¡‚²ÅûÉÀ8&ôhýÀªb+w=Ç#©X‡þÀ&FñSÝ¶ëº ¼Â,bñ§‚VwHŠXêú f0ðq P>Xß‹b¢›_Jßÿ®•ítä:VIð›ÆMóW¾´oý“š7M®¬^™¶øJ8»ê‰âFûö aŸÇQ“©¤wO[p"þžu}{íßÈoÕɳQ‹q]Ȥd¥I_Öi$ã ¬Q/Ï4Dߨyé6<ìCG“·ØJV3FûæÊ‡ØKk½Ë⌽{9>ª öjÐ⪇Y¡Xvhâ¥D¹#¾±5ÀÜ×ëü–ÛP^uYºLÕïˬ·FàaMVkh :†ÁEñÿQ6‚fØŽÙâƒ"˜»ˆ¶à%ÇüûlœC’S=os‘ÉxÞ%Heƒ«g(É€µaÚРð²}à[òÀ G×§ã3ÊðøÂ«ÊïîiÖÅc“€²ñµ‡ZñM—H°oN=,bBSùe˜o•èüEUXñ7ã×ò)ÞB#ÈÁr¤"û]iÖîåKà멉¢>«€¬ðÓ‡†ª/šýϾgíÅó;à××Û ³;šeBç  ºOÅb ÆTi@;vÒŽÏNø$9;:o›Ìõ¹ÀOM@PÎF*y™o·ª:Ú2„µª#h§å1 ŠiëvÊÄá p0K÷ãz3ÐÛ¾…*°sÀÞA²Æx4FáKÛ{K°ÚÂJºüpýk…™z½k¤RЦƒ7 :r§_ƒ|ÄØvB&†Š·é¾5BÆ#axè*Ü‘žE* Rr5^ë)ÊõÎw´0p´>†Èî“tEJP—8Óqv›n¥HÊnÈ„EZçkbùTºPg›ð[|‚†XÈ*S›.jLIC©WœV¾ójöË]© "ÄÈ×HòŒÅÓ(xÝ–¼!Òï }Øä¬yüöÏ—ôî±ñ˽¢tdµ2”±ÉûŠ’^°gH’OỎ#ìV _Ü·iwW5J¨‰¬‹4ŒiÍÒ&QûÝù¯‹"Z^˜ÃŒ9½•¢©]E “d°ù$ÿµÑÇnTÓ.û±1UÀ(®-“¢vÔi}¿„»:ÑmcýP#3€çãÅñhm%R €ùx WšUéÇíïÉÞ©™äDqòx÷ª ·j!‘DôÚS“Aà ©’¥ËË»j=ž4üõǶÛêNfÐ1î;í” Ð:†¤ºa`õxô ±5…ÑûÕ#=p ~î¡îoT‹a€å¿„ +úŽÍ¼æóàOÞù ¸ùå „CžWXì(n[_É,i Òw+uÞ”QTæ;0 ZèÓJâÑ&×ÙòötZã¿E]è¥ï•‚CFißûŒÙMO*Òœ‚z†P¾g2ï(~i¶Ik“xCr ÌÄ$46x}#qI- ä±óÍߦåÉ5¬ ÏæÕ¬ásÀè­“e£†ÑC§4@ƒÅšÆ "°ˆ¹ç]Àc±ÀÕˆ=ü¡ØÊ÷°CP¬£:˜‚-?ÊÎ2Ø¢DdL*3ö`bRti}_)Eqæ>ê ŠtAX«jýØókek®,ÍKü]{Šn/¬8Œ1ë±–ŠRïÝ+/טóR’°ÛÁPŽ iòW+vã´ -<ˆU3IËþ¸¦)Ë>AqÅ•Àâ äãX÷–¬E)ø³:¼â-"ç…,Fq<©MËj)‘äN@p¥w·Áòm~ô ϱ;:nSÒ™*7({e¸QÇs]#ýÜRW^ês,éˆ æÒy»êIv¾ø ÖõÄ]Ú´ W„°ßÒÂŤ#î·àŸLAlŸK‰ù2*H‘i^‰ÕIê—2l „é‡znÑf\üs5׺s•&/¸$àò†Iü#‰^à/ ’Þ1h¿DÕœ–r%•Uö-jãB{ƒ´nïœâQž¢{h®ÿ]![1A´§Vš3Ä[Ÿã½™Øí¡,6¾öÌA æÒvåD´•yM^*»·sâ³õéleÂ!Õ®hÐ k «¿?ÚÐM•*ìL {V§ÊÕx»ÒWz‰nÒá>»UB ÷æ: §h|Éû p9ÏP^18˜oEž²ˆR:©šÉ*½Z„ŠQ`„6®Š ó/î¦Ñ˜VeY9@T¹5íCjøvÙ¬:-»Œæ´fÆ"— î þfeçÐi› §yýBïWêÁÀô&’Upx8õÓ×ô—i+Î,mŸZÜÈØX±"]û™…¨]°»Hr×õþÆOžÒ|ûPÀ&˜GLãuÔ<ùœç¼d:(¶Q1™ãš,Û~¾Š†HÆžMc}ß›< óïæFš‹ž‡pV&ðóß"bÒ.3xø¿CEë²fL‚¯ 0¥aÏâ«´øÖ½+ Ò¤u¾ÂôØØnmíQi(7äŸTˆBØÿHâË1‚n:ŒÜÎá4‰ÍÀ £*|7¾ÄŸ%¨Z_—c«¢úWÒÿf ¦âf8h.Μ6 Ï,j޽«áó7Íä˜é]pfmþ ®ö'lÉŒyÆ\¢¹ùà(ßh ³a¾|矅"p·FÔðsµù§Pz࿪/èOÂiü×¼ÿe¬ï)j.P¡‘µ#X‹LÍ$Qo1㻽Ex§Eï°;†\‡%ö>‚"²SC<¯Ûmí9><Ò­êHG=ùûâ{HKJ€þ£êUfŸÜyÜbÞÅPÕìQï¯Í“­K÷Ÿ¿BÄ¢¥dùRPå»éIv·Øäa­Ptà$*0X¶Ì›Èã2Â`uý%¹C§íD œ´¹pr?ŽÓ¤\hûY–ý󈦫$벫ÝuZUG>TĉPnZÓ"öhFÓ\¿s}#ç®"­"ù:cÄ®b‡q‡å®K‘}@Ò95+ä6tÝæ0áüD¿ ÁÆr„xî@_&¯$šn97¿uj~a¢trÁ¥VsfÍŽ¿þGµVÛrÓQè‚¢U’"”î”·oÀÆ)FFÓ¶/9Ø„ˆÏî YQºÒ× pÉ÷&Ü4¾×°*Il?/ˇ!l'è$õâO“ªó1M­? V-G’*TýU—w½¡2Ó/_»["®Ñ/'ÁŸà˜—¢è`({ê/•pWÆ‚à{«–q(fk,äÑO¿ºÙ÷¤TeZÔm£‰x}tÏœÆ{LÁžôeZŸ -¢Ž/`}_HFy@õê1Ø„qY}þív|<#ßc,åÐ;ްƒ|K4ôN}å6ü– HÁ¿›¶ÿO +ØkŠÕ9 õó¦í†ŽÖ’Õ¼©Se™ÓN-÷+F.ˆYQC1¿ño\µ ÔºpO…m_*FD>êäòµE¢ƒ~>=oz YqFï«(ú©«XC´x” š­ó/–°¶®A¢žu¡›6’ âelž°­£ç)¤iz¯fm¡ñÃÆcÂÆÅ†®%tÁݺ´ƒ'w|^¢%n¨wv/ݽmUÄó+CÏé„,ÕÎx ¸ ¸]Šz°£SâÞ»¼ãE)LZ& «$Ã-Ì]–-(@þ°NZ_)üpÝî”(•?ôÈ‚NDMCS–ÿ¦×5ˆ)o×íêÊs_(1 ó|$zÑÎ þ¶=Ê+t½x óÀ³˜÷·…8c쾤æ.ŠÝz záÍÔ)ÁJKœ§Áà:Ë*UmïD“§Á=]ó jœôï‘ Ry ˜¦„‹ªšKWê„è¹!Y7¸Ã§s¾-Eã0¨øàQÙõv§ "ØLd~±ìÍÂL¶b)`ƒftrÿiÓ1•Y½=1ìׄ¡äB£Ý* ¾7ýwæA í nš·vÝuvWóœj_*aà¯c¹Ö_'Åðäñ™ð[—áJþ–ϨJ»ø!ù³¥-„Vç©v$´> jL“c¼)RŽ÷´ÝÙÏ:À,åœÒg}ï?UÇï÷ x4ü¥§-绘Z@ÿëܼ ë:Ÿ–#/ND×ÜÆÝ î9yò³rèã\E”w›ÑÑFY¢i~=r 5Aöç¨ÝÖñ8×{eí]|ºQ¬vÝÎTœôwþp¤‚„UÍœèK[S»Ñ_ˆºzãð™Ü܃b4“ ùLM"ƒý/ Ñ: Â&Á nZË:œk\›ƒñô5üW ¸d[u¨=‘ìƘŽqZ'.2¸Áž²c»ß².ƒgwDÎOŒ#`]!u$Ò1º³_Ü*ã¦í‘pzHwudGB!÷ dï·Ö¡ØXÇ#³ÕÓ¿ÓiÇÈY”<µ;Hé5¥tªÆ ßC·ç·YÔÚ³ºË÷wš5kœGñ)!UãO`eCÛ¿`…` ººÕ×Óêö­…[uA}¼Œ3šáJ“&¹ØZ”¦ýáNì:M ë|¡{ËBqÎñ¡oæAÞ’1q>{u‹”Ž¿ÚR „Š~¬d]æÛêu‹Ú+7ƒ’ûã±·T5~d/ 05ô€ìƒR1ZŠlþIp© O; 'ÁÙù”¡ ´ÂÝîÖ¨bgô«0ðO[_e ©ór‹jp ŽOSc@€þù¡òL=­åú9kJ€"ÛÀˆýÝ+›(ª·ÏØ}}àµÇ; {°i©%£S¾4 []ÿpiœÖ$6 —–ÎXk¦úˆÇUí?™ØÇ6&9žK¢ì6±¤>G˜¸²JŽL‚#"äåè@b†H×rÔK^kïzNêÄI×)(Së_3êOoê«×$AblCS?ÈEßJÇR&\¦íy©Z¤÷ìÄ µ-DÌP"“\k\¯9U½&°~4—GÜÄʪ8#&@Î’%¥×vÁ×/Âà6gÚ¨h%G,}n1B6ŸñÅyÖ·±I. ÜpK¢,u[ijú‰ªsj5ò÷8O‰í;ÅpF›Íã4)àÕ@?Ÿ´ûI…+ôsh-s½/ÊÓSÊŸ±£k×óöZuvEÕŠpgmZí=™îfEz?ÄÊvtü‡FÛ$à }t`3;¤$'Ì—u÷Ò\gØÀÍð¦ÚZ}ãÐuÇ“òzè$ûç •¹#A ×Þ¾*/¤=4ºÐej-ìTO©ô_&ÄÔÝ·Ž~0ì[4y¡<ŒÏ©Æ Ïèø¹æÚc>…Ñs é< „CÉHè’›‡·çÄ€…VaÃÛvtÙ\Û'²sâÍÛ‡˜ÇK+.ä¦U$²J]†µPF“±8x&þ°S¥Åb*/È«Í$ÿɺ8yØïå[b|«zuº‘O¨G­Y8ψÞÔxt“¨}jrÃòÞ€ø QØ\—ÏHmM­Ú;0k+êâ5ü¼yâ¼–³ôfÔKùë3Ž¿è·(Ê-VxšV,J~{¤» œ£^ùA0Xs²œ[*yBŒŒdñb7y5gÏI Ñ×n‘BÛM „.üz7=éÏ xèY$(ýõ´ Á™UÙÙxí©.íDùNO©ˆQÔQɃG8ÅÆCâ‰OÉô¬Æ ¬.šX–·¾êPG°*Â;Åu5û È ;ÑDRHè|¬>rìW>-ó(²³Ðŵ¦d!×aÚQs ˆ€ÙÒJßz_ŠG•d,¸ÙÕô‘ \e4úgJðªÊ77‹;6ÂpXŒÊóÍíMõ Ú­Í@&– išq¦NÒd›uo+^y}ÊÇ﯑t˜õ~?å:ŒœÜ|zãú˜ýÔ¦}Ž{ZÜ~p•ÚòàÓ0ü&èíËñ¿ÿ”‰ä„)(~tší’·Q²Q¡Âè~o]¸ìö°•øé=ðð¼ANúsiÃFn[©LÓbÖwÂé½_qí¡£³Æ c ‚U 8ÍÙ$Ñ`â}n{ø”Ô\ ÄLÄS€¼ª«g€EÑ?ž yáî†ìèŸEB÷ýá$þÑÇø+O`* Gœ ëS•oš” uì›NæKÇ; bĵkÎâM;Ó7¶OØÃŒÊ±…ÍØ o‰âöF72ŽÒ?È*ð\<R/©÷? Ϲ}¹CFëþ ¦´*ÁÛþʲm‹½V1P}\ø oÖ‚šRþå‚Û>VÑ¿5ÃñpÐ]:^îá$4Çoë0dPÙ‚‰Œ’£.—Ä=è$IÙl¤´Üößxz|ãçmx Tà“Ùsü»„ÕØ{lÊì+÷9½ j\šÅ¥<ÃWqZqN|,"88.•ÑpÑ c†YZðÙcÁ©Ø¶_…ýRº'ęőkþ–—»ý\¦Ð±ˆ¼û­Ýnsº Ȱj¡EbSæõáUœ¯ã'þl)^ð°G_}`$Pðè³åBºð nÒ)…ï¹0Óì˜òÖ ¾ÁD{šºš!]`Jw)¶Šÿ+c©ofÕêÖ_n~¹éç'¯nX&¿íæ W•OåzÌ Šm§›ÀÇõRøFÊÍ‚ÞáYW½*å³wuƒ(92†LÎ/ºìtîîv̇õ¿Ÿ/L!ï°©½/®À¿!>Ázn?þ×(9ç² ÒÚ³“ò­¥u¢éç@Äyù±š§–ê ^k“òüõcZ@Y˜`¡¦ºìXe c¹Cj°‹à„Pq cÕŸòy°äî?æ3Lø˜u‚Ä­T&Oš, +È|- *O(éË 4TͶ8TH¨wÎÑHe Ÿ; d¨<ð5+%úp‚[¸dþm£‚Z꫎“Ð)sk´-¦ŠphÀ$ocZ‡ ´^©[+† œßà|™ÓÁŒ. Ù£º2» 9#çðí&ûzáþ‚DÌþ|ßoR7ÛÔðïgÛÖíéëöôùû{²ˆ‘á˜o| ¥ƒÑ»¯V^ÿ@;òÞ@«ƒÇyIJߔ’Mø*§LPÕy3!×ãR»èkq{ ²Øa¬F07Bþº_š6Âݺù¶dSqÁŒrÊ•Š›žle¼mŽ0=½ÒSª5WÑ…æûYd¾5Ö¸ÉBÍæ¿À¯á}YÇ”ÎÉïHTÈþž‰[4¡ªnð’¡w¶, GBÞ‡ÿl$u™ò˜'°]àñM§"Rè,ëøxïïþË‹Ï ©"cc`n†‚SÕëÄSGog‘Nó×>X.°U° &’¦Jq’Tèð…vðÙ}tœˆÀ;Á»-€ 3Që'ÔtKša´$€e'Ï-Ó,nŒ Ý&U}·å \È^²Ò÷M6fŽ-Û3Õ¡Z÷ætõÐ;¦ƒŽjò€z¨Ö ®zÔ+9VÊ30I"Å#B¥`±¥á(Û•¬â:ªSU¬·–§uóap—·£˜±‚³¾wDÃÁÂ$äDv¸cÕîrû’Ô·Ft95›.,|±8°á3veCËzÿi¥<®¹„úpž6ÅAÂ_›ÇeÇk¢’Ìo­û2¼^#8Mxs©¤6ëx– »‰)@í£05Y“Äö²8Î èýÆN63ëMsIÛƒ¯±¹XH;|¶ñ½ K¬>gÇ6îSTäpºD+¹xKà³îEÕµ+ÞI Wx¾ !³wv&°®‡ør×'ûÞ&…2õH‚åºa¾ÅÆ+Ò³œ5Ø_/:bÀ÷@ºOZNšGT2‡{u¤±UÙèãüÙÈÇÌ àØåÀYy¡—Ø—0Okf¯cÂJ†uÜ7ò!rkëš‹°Õ,¨Äò{t—¼5~18qâ™a‡E} %ì#e+Ÿö»4å£AÇ]y{¨DÖ%u9BñØ_-`yç¡à,óÑiëA!‚<ÿU3O @—A@{g*òÿ\,Âg ‡Ž¸Æ^ÅãŒ0Ÿ&V˕ힺël¸C­ÔNÒ¥¬\‹¤ï¤Œ‡G=è §òú°š}è7Þ»´ìJMûö“}¯AÙqÎY,õqÈðÈDr½—̆Þ]l©¼ß’ê!)@îç«ÕVÖ.EÂÙ_µ2't>ÖWGØgaÀ¬ô&£+ÿ2/œKs^R0ŠÚm–é}7hý‡ðó‰WLÝý…ŠGÔ—WÃ+“x‚|zÄö˜–ÒVÌÍÕ3½šöصڧ füæžMùüð×Ãg_:ŸÚDfNþbøÏï)4 ß"#¬qoÊã"ø¤Ú4ÎëÊfò­Ò±íÑZLcª®JÔâ /íÐj_œ7©£¶Ÿ„Ï;d¼ÑwI‘,€(ŽÄꮎ:8L®@r2ãŒùs“íy_7¨–­àhûÛAœüâ§ôœwA-è×ñŸÑ"²”*2ÌŸñ#íßQ9€„¤£àÎXœR!}–{’‡4X.&\¯½ GQÎL¸,˜":r5ó\1l~Q±ÖpŒè96zÖ’v)óš!'ç’Ñ}ãÔyóÄI°×%Ì–_þHþú<¶üÑæÏN|^î3¬ kß7£2i.îYue{ú‘LOtJ¤ðéï#˜6®Fw¨1ü‘ˆ®’RÎÇ«?—½Y•ÿ#Þ#ÖYªÚÔZr8jFÜÐ[èsöuzÀ4'o#û4,ÍQïëÀ!°/Ú ‘ ê`ì¾ÙDÖi8³á·×X’æçC0h†{VàWÔÏo Ó«_:k0(~CÔcÃtð4Ùk 9…ë%ß¡rVæ($gGò?\©/m¿€Jþf¯èv¼öôQ^ØÛK6*ʹ qNå[’ ”àIÓø³x€Rô§ÜÒÐÛ‚À÷®"†â/\øÙÿ"·©Äûb‘oï†!hšò!¼®D¥½ŠA±(àaLH×Ç §Rû` †lʆr†ü±iÀ­ÒŒ=R§ÄýÑSì#WCí­{ Öˆ±ð›Þß-@ÛŸë®Â)W }çØˆ®™Jn7 y5¨¢È@Í«-­8[Óæ$£8‚ú™äƒ+Eq§â݈L×fsÍ6¿Tœ`ÓèêiK/ â81é˜ÄZA*’Ù¹N±ü FO>™× áuiº#±«øë¬å|¨?ilOáØFÜš%TW¢#õlOboL±>>?Yµ)ñÈR¸½‘eö¶ÑK•‡)A’é·t试¤?¥¿ÈŽXW‡´PnÏÝùk!äÜŒjX[óšäþçè­Eóç3.!ÄêÚ[BÇԹnŠf;±~gB´‰ÀÙi^ß7îœÂ~IXt|Í‘DXÓÞˆ©Là°>Cöèw$}`·AÉ×Ò^–³âéž!Úü… ÷ #ïÚ¦¦oYS³­Fü³ä.Å:2e±®J~€¤o“铵¯ìÓ&Wv²*•’Tm`›ÿ:¡bÔÄì‹›,€Õ[@œh©m)[úUbºCx“@öž'O;>¹#¢‰¦LiB"öTåÍt¾‚þ&]ÉmÎFàܧ*nŒÞyV°bò.ÎN˜ˆá_üÚÈÛð—ÂçxƒAðdašËú°ñÁ¤7“¦Øº,«™Y9{³ªŽ×Ê'ˆÇt»)·©¯/ò–1 <Ågy%©2ô>ÁXÕ“s°IáÖ{Ôñ¶‹\~î«áÏ<}.8GÜU©œïÆS³Šßj¹æevô(¹kÛbd²²({$.L”Ÿ­¡áÎaA5‰Ò%Œ­¶NqXzxÖg§€<̪„;MO”b€¨bÀ¢aõ¢/«ópîO¢°váMòIÙ´|ç6$ãÆ„³žh¢{Þ6þx¥>ü­/0a¶«ß]^W=ùNÚNF€Á}Z¶|NßN°Ñƒ#vAhEÃuß§D.¼Ûë8ÏßQ‚fÄ:öf\fZ¦þ:û o@YØ ý¿ði'IIf¨ÔI‹D2G@µ(Ö+àÖàŠ¡„¾UóoÕÜݱ Ã/gPž¸òáK³ÿ³œù–@ÐŒ<:¨Àê§þU\ñ ŸÐ€.rÍC¸¼q¹Øåƒ˜~=VéÜ×Ý,‚ó}s3ôþ…Ê+»ˆ¤ÀŸ(†Ì¤{7ÒÀò´[E¾–HHÀP‘-’v”÷ýú—_•Q ¬à©Í¸T‡(sjŠ€ö±ÒJ©­Å/‡ËR²˜Ì‚Õ4rŸÚì3èÞÍ*Õ~ìˆuOGQ|´cOß´™®ðk¹]H:›Ñ²oöø€f«©hëJ´+(ïBë ˆÄ[›ßQÐlÆgÞs¾Ž4YOÔ1?8 v]‹#‹þ/wªÐ}´>ü¬–ùàËŠ<¢ÕjŸ’œ½\=ÎÉ~¿vœÆ²Ü@• E¿ 9¡ÈÎ[O,yVÒ¨4¶­XFTIÍŸßnwî %ï[˜¤pŒH[Ó·M}¤÷ÐÕ  `bøB”Ìï…ß~=Eš>¾x4+ê>™¨žm# Ä[ç}¿c¤l§°À”ÕÞïyÃ"å¨~–ý2"r‡NŽŠ`Ãâ~Kdª<›+n«Æø·`pf€}ª=%65xÇÈø{‰z.-#Û:8’Ý¢†Sž¾ýÙÈ6%é@ÑÃd>²Åµýá÷T¹A õ<ÉR[˜¤ð`!Pi8œ˜Øà¦ò5BµL<ý%Âæsá×Ózy>TŽlÉF)0ææ.ÐC@ç)€¡ãlÖTùÜm¼ú«¦Ïü6ɲ×mêÝpÒ>Wª ÈÁÙ¿™º»b +¦õ¸š¬vW»©¿™[‹`HG/`k6ü6¨ï¤_Ñ7„Â×8xúhM;eB ¼”„3åoÉ:Íø} ¿Tú;ýÇÔC«É:ùHîÔlˆ´`â¯õà°msæìlÓCI Uß4C#«V4J7õTìŒÆ¿¼ÕAõpÅ*Ë“—ýYìz '¿Îþò—3Ÿ}MC ù`tA´F“‰4=¢¦´úTù:elTufÿLÖÅSXEغɬì™:ÓÜ.ZÆ¡e¹L2áSžZ%e­ÝŒ´jV$IV_@!œW_9­}Ù¥ûo溴•>ic#ûÚÍRÄå‹9 ͨ,³¿ÞD¬£n例ÓZõr<ˆM°Øk)ì[æîÏÄâèyGîb1&¦zÍ»ÁAÿ7TImE±£ß¢p}p˜ W„x_¢ðð’"«e …àÓj’Åf'ÿ|ÉBðåØÎ—ݳN3@B±vnòŸÖÜu0›W4:©Ui›O$_<| û&ªn:Á•µêk .J] ìX8ÓI#䓚VÇR ñ•AKÅÌ*ñ©y¦9QX#§k€³¡¶æZ[RÈ@ZV:ïdŒLD0ùÓì$øl\øðõ æ¿8Èè §€µÆÙƒ‹nD Ã¬I I´~ŽÉÌ?ö7øG¨Ñe}H½°‚rZ!à«Çª|º@Ô½tM ‡$ìŽä¯Â¾Ù [©Z¶‰Ç{9½Ï&Ëæù˜$êO>NÅ_bé_°I/à÷ú£â¶tÏy¾èäD “–&9.Œiã^Å·¿æ^~ÅU0&âë°fuÎkǵ/È$DË Uæ(€ÒòDShc>û½ÈÊøh5÷ˆJ„õÈý훢V¸VK©°É0㕟 Aûª8˜þ¶g¾áeë¿`wÁ»²}&ްàÛµÙ”÷vD»ÁQo>ƒ‚×°% ZÙr€Œ,åžQkwš|¢µh·?Az7¡·@ørc8Š–#•J5Ë’™r#² (ÏÚÝ3†±]ÅC42Ñ‘¢—[`¤ão”)Ù]—‚U˜[Z¡7IФåé­›Šê¹Ï±0”ˆøói” rYÏwiÍľù¨õ©œ&å?Ò¬Ö®©Ñɰù°ùa u¼6¦ýé6'TZÙ°q+Œ^WàÇÝÜ«Ì RIzdvËtK ZÎ)º9glqîC“ºq˜½y_F¿v› ­j¼à|]$ކƒÎ  ã;]»è˜’µW3'ÍÑaKÑŸ467´ÌÌJ3TÒÞµjkþ5ƒýÂc¯wÅLTôÈ‘b)S¿™\a´G7Ü`ò&;„°µ ù $ªÆ—ö˜þ룅ž-]±û¡¥NÁäÎiK×*7¥ç¡¹ϸA&š3È$´õ”º&ñxÞÿBH|/ö®Ë-‚¤žÀÃ¥z…·bä³Lù£êR“&”:Åæéü~4²J«' þš¯2”Hs¼Ä§óZlÿsõ±‹ˆßÜš¨•HØaKÙpF,P×b«|º†(°œ‡z8(ÄlHÆd×x*ƒ¨Ñ­÷¶s¬Ç°6Ìßå,€åæsD8êsÄ0¦ ’"2¶¼´ÙØä>ª‘KúÒ€HÂù¬ÞåÌÎøÂʹñ IV/«ø&zAÜ Ê5¬§öÔÛ¾K|*§ïÀ§íçÊÑSÏ'Á&[Ô™/8˜ÚÆÖv3¿Yj5÷FÕú£î⦟>?PcKžZH‘=·w;cNóìÆˆ-CÅ faÒk;s'SÕ_|龈B¬ÅÑD©Ûϧ1W¡Ž¨ä¡m§VG×Q¬_ö‚÷â-ƒ…®ŽéÙ–.͵´ÔÜÐ|q+ÝÓJÀì»ÁøR,ƒ32÷¼­ÌÕÖœÇWÒ=@i.¦ïÚ `Á 5¦7¥ìÞr(‘›<Ýä`d%tñ³ëâ¶/ÀÌÉj°Ž5<Aåó@}( E”f X€¥|;ÑÿQÑh}æõ؈_ ­añ²‘Fî9 >DØNÛæ:j?Ë*"±à²óº›>‘Ÿ)Q|ZçòÞ§¾0ÝKjæ±1 1 Aä)ˆäËÅ.\àİBå0Ódˆ½ä}0<#JÓUsŽGö!<]qßÒ‡á¸dÎ?Ý/VB£h4ÍC,ÆŠ•v|ãÅJcƒ{°Çz-„v•ò+Fkm×¹êØ†SÒCSÎú ž1ŽäG†)¤„Þ´ë¨ðƒmFÚ!§êßUbÇN§]ÍâÚáh~ûMštöw,#D³ñØ äZ/ýäè<ˆ ˜£mWËßÌ·zrÈj5Œ s"ØýÆ'7 Õ¸#<|;pô¨Üzh …&œâdÉE|yâ(Ô¿”¿°©¨Wºoê-~²ÅAæ§Í‚"H°nS»¶*N¼Þ=â³1âzê>!,~ËÅö ®³àç‘Xhæ‹›ÙÍ/]¶Ì3Ï'j ïéÒ(ˆr¾™-Z’X¥Mør°÷X4µCd+m_.t=eŠËŠRøMTÞª"œ'çûØá¸QÜßÚ1ˆÐù2¨{ïuÉÞÜŠ@0‡3àÁ§­Ä¦œ}ÈçÀ˜‹”F‡ç9OV¥ P±µŽÒUÅÏ<-m¤_ h¤¦îT½ÐKÖÊ¢è4èÖKþD–Ý»˜,Šˆ¥g%æC'ÙÓ¿ ¸ÓÛ/–LXOW ž¾·:ü–äR|ŽÐÿc‘”*Zs†A™DUãŠ'*ÒeÝ1ʤN£É•'ð"&6=-Ï£ ~Â7${æ1á¥É\iÒ' Cd@‰îD +üp_:¶„“oŸ8¨2°¼n“®Ìîëw«t˜¿Ï²ˆ)?W:§ ÅšIø0‹U–À:‚޿ܨrSñ7ᣰÏTr‹¶?ÎÏœ† _%4cø»É± ¡Eê€ùÍÝãíió$O«ô‚€©êYˆ$ÿê8I•aØ t^ä©— ™Õ…ÓµëCöÓÃFPsǧóujejH 2–mÏs{ÀrÑúãáã80´NÓb\Á cé¿ …É­¥ç}’Cè'«oû"`”Â7L²‚ßxå#¦îÖ&æ¤OÑvÅZšôGµ’‘£]¬2$4ǧŽ(ªù^=ÙÚOàk?n†GÇBØc¨ p_ok^z•¨iE–†$‹ö:$e3ÔØïKƒž‹‚µ­Êd…‘cžÂñ÷l@ìrVì?k¤W'„Q +âqU+ŒL4,W¹ûWM ÇÙ÷c¢¶ïL8„7ä‰ì_‘¤¯9ÔVßZ¯ë·=mÎk ò§ãý/ ¢î=†Á˜JTT IJÑÝ †Óµ3gEÉco¤j|ªva"¸ò`®ÿV4¾{š©©¾…ìûÄ[K§Ó¶KÈTKB›•(0ý.ózKFi2¢C‡:ö;P¤D”>°½e®dãZï;xI±üþ Té65•\éšá'ª2³›“6væ nÜ¿âç³­õ.Owõ”ã{ìËcaLUÌ œ[$v#݀Õ‡%Oa.Oóîáì6_íha)f"…ÞG#Q>ÃŒýuæÐ9d77b¸°2ÓŒ Ný…%2*GsŠ-÷A Ó}÷ʬ±FdÌPÇÖ`È#‘·íK#“†K>5‹²`’ä½Wöóõ‹F"==J;iŠ`vðÛ°UðÓÙH¯7¨E<Ún²Dd–=öÛCf L‚ÿHÚLOH֨Ħ„Â;­x´¦8"}ÐÖZTD"ÕÂnåF›½6ˆhãtf'“ËùÈãšÇÓâä]¿‹6^'þBZØêÌ(w¸Ü+Õ/>3NFG¡M£KÍik #äÄýå~œkyALÅDél‡Ú}Ñ­EÙåz£sä¯Ê‚"ü¬ éÔ»GUÕø/-èÀ8œ(Ù2ƒ»Utò·AÌ07¼h%‡ç—á5Ç%N¤V'ÖêYYÍÙkQ• 9c`ç;£´Ê Æ†«õHR½5"{§‡ðÜÛ\H@ÆwbÆ\”äi’Éìøàî¨ö·A©ýÀdk Ž Ãýë™{Eƒè®Úã„´gG[£Òu‰„8%*dFþŠã­†¿«®LTV[q5q~Á ®u#_C]r%¨ž@ż^\®9îxIŽÄ¼.¸¤þfzdY©ÿL„tíñ-ø~²c¤¬’#-} ­H9ŽJ}#Çd³Z@Ù)²«ß ­·\0haY>¼óõI0²IÃêì*…“£ÿ›Wl0ýÈ5Íf™¾N$‹øNRN¯Þ-Ú5ã„Üù>Í—7«½eCÉÞ–ÊŽµu£`ß½—eC™Að:±V')E]©¼£G/>ä§•ó¤ùyÆѲ£:hcêÀ—g0ís8?Šî (‰q˜’:ë€w±:s¶oþå¾]24Ÿ—Emé“ 9̽ï¾é^‚'Új, ª—`IG§©<øþF\}\ñÓ×ÓI–|dÈÖÆx×%j?µjaOt^·£Y0:qo~ø{|1,¬û+¿w`â€^ƒ|o¸“úŒÔdÚöDÌÌÅp†ˆ¥3d}>Þ«ÇÚIú¡ZGŸyä'¢…qÇY6Iξ%„„èòHA%*ª¯ë¯ðÝ>É}Ìå]¶æî2”-½cªÚ¡ê²ØÂ9K‚íÅL;®ÊÊ(þgB¯BÓ¯¦—ä.~è_E‹|[Âdí%ŸRúÕz Ù›6[á¤C‹4«‘V®0,=nryævø “Y6éF ¾Zg'ñ{[%[9š™óˆÁü‚6ÃHdéZ’-‘Î 0Âi:±F-αÎe€ïÑÏÏl”ºœ†mb°ØR=L xÓ&Â%¼4e¿†‡ÈÃ_Þ˜1Òà´Fµ, \÷»öo4‚azºq)0e½z³¶çN¼Á4޼óÇwY*i\çU—àž˱'x ¯ÕgxéÃj ÿ0¡|šÊ›ý*¡†+¯€ì zqkL‹æãRÉÒB®‹-ÔÏ6³¶iq(."ÑÃÝ¿R,’Ö„`뱎Ö#0r:"’à÷tšcWjcÉ…2‹Ú%˜[òö ÄÆÄ4saï¸ëðÌ–¢-ß®‰2óBxÖU59-9: ‰)‘½Š¤†kK·cJHÙ†„™‹3ªj®ïuþmXUn[ ÿhÛ\Z¥.i;Ÿ¨súFü“ìhÓ{7ˆ4ÉÑìà¹Ïã †•J`àõŸ °ö :8⚪CϨ¯Ð†æå{í§R‡¸¢lïðņ\Ô!Mòqýõ OùĸVÅ)OŸ~éxk¾D{Ö5¯Æpìÿ)ê ^Š¢Ä"0M,yò|ĕˑ'SöÇI”h“˜@¤•*0,|7x<ü“⺃Fã0[4û­“}ii€§ÉùIªeCB&Ë—x¢¿+¥®¥+3¬—Jæ1ðX£À ñž›2Ë èÈçy±Ê¡j½zm›¸Ä3 Kü¿>?ý{7b*t.o½åÀD¯) lTE!ÉP¶(­™öðæˆû“>ŽSå›ì7aÈüE±¤ÆÇ4ÿ)ö×_“èØ¼y~¦8#®ÃøÿSïwäÀ ¨6¬,™'§¯!ºdSÚ5®¤Dð¼˜JßÄ=@b2¤(­~J%¡Ð¡­…ü#9\†ÌƒÕÅåØÓ•:í Ñ"¥÷W.$ýüš1þ†•äQ÷øqX¢DáâÐÌÑÃÔ'Ôë1@[ýÐÑûìÓ=»+ZU–îþ[ËKÖxwå×]ÿ+ìJfé[ºh×ãJÅ|LDt!"SEY W©œM6!¡œ´%=€RÉAþÐIëÏ! ÂyÈË"Y]%t;7ÒÛn²O±I©ñä:üC”Ëv6ofî7C—“ïJ¾w+¿×²»ñi 8—Šù)ùuI~¹ò¹˜ šü)*Ô¿¹¢9 Ñaµ¥vþ5V5ñÍyù`p.¬äW|‹Ò6Ð&dPd9Y_ƒÒ¡HÑa8àÙ|y²»SÖb_1µ gôk§%œcUÌs"pÄ :@øtµ‚„‡$âtÊd”ØO¾Ü4¼ ëÇhòÕ¶­iÇò²î5ocr•¬%^Ãч£%N¡ ïÚNÕé ^ðQ"÷ÿtm6UkAš×ÊÝóg~g· 8[ð.Ñ!=Û¼=`QtiÜo†HWv˜¨^á9eJa¨WŒB¢Ñ‘ÍŠ"{E½æQ[, ­Áµ!¹TS2Ù©Awn“â dÙÖåŒï}&µ`˜0+“ékì´ÊÄn@“ûë‹ ‹ß6H›O…6V¬6ÍóE[Œrk‡[¥}ñ­ît§¨-ŒBMëïµ ŽPжnfˆ¶°I!ŠpiˆK˜À¦öê@,-Æ’á ?º#'¯V¢æ-@âàUÉXp3,M ŒÛæÇ”îÍ}Im©Ð÷eèd+‚ ö*kü Wòâ,Ïdâ«à!ßGÿ2f ý{›˜ÒSݪôê}LºwUüx²9¿½-/cÖÎÝŠ©Ñ ÷—ñšƒ‘/å-pMo™¢·ÌE.Š`ACM‡n¾äævãcÉÙ­Øtlè` Ù4Ï6,¼ ¾G<cl³¼`> AÊU ãÀñXgâä(ÞLÚ{vjz~}?Ïíè+l|E’–’sþ­9XNJ»&DÓêy*îé w þjš!§‰ßt^ 2Rh¹Ùä(ðPì]4ˆø¦´§’;ã+¢“­J©Bþ'—ÖoăpBShŠäh™Ü0d4,ÒüžÀáqƒôiÆ!´“I`³K3VY|1 „8Ú¯cõ'@[c Ž[H¡»-û%l†îã¾n¼;÷Àäܪ ºšMc2? k}t+òž‚½¦ˆúP™ï»PÓ€¼MÁpU6eiÈH¯ÆIOTüÓrÕ[3‰!ƒ¸/Ó1+¶Và' Âþºõ¾}É—ú7órNµyK-‹@…ÖФqÆgþ‰Õi–9Çp³È†§¯†{%¸Ïr«,ºÔãë]Z&LCGãÉvÕó§*}(ÙÉ 1eÈ‘+T)ùhßОé ^täN³ì¼iJ<=øÉ„LQ²p˜›sU±©iÅ‘ÏÃ/ŒÔjÀÌ¿ j¦XÓÃ8NÐÈf¦ZÃî°Vžt/¬‚Ç£nüý¢WîÇ+FÉÈz¾x²]YZÆàåEbÕ…r ´U˜  ŒvA¤2ÅÆ6–߃´lÚ€¶ó³TÅ›>Ø"£±h¤SòÅ+³Æï}ôãâËsÿ?ú`TJ0•˜@UíäÛW·÷=¸û®Úû¾7 Àç Žk„Þ'‚ÜÓ±Œœ"X…}Úê#ùóµ¿Åˆ]°ŸË‡–£™ÍŒÞ/±sY¡LºIùJ¨™À%®ÅâD õ¨†ýã[Z„‘-6x á–ÃŽý÷[ŽY‹w5…ä½Î`bQ ·eDýW¦:]çÿut@xsÜÜ¿«P¶[“öûìÔ®k<6L]Ö–{(g­'ƒ@“.Jlî{mBmW™>ˆ¥| 4ù‡œM•>‘NËèP¿vð‡g¸Û*ü¶BJök‹ÒN‰$èüîA:XŽqøºb§öxE‡Áx¢k|äRuSýAu ’yf^ÇNWÛ/²W»ÿÙic09OŸ jP ‡ ftypjp2 jp2 Ojp2hihdrcolr"cdefjp2cÿOÿQ2ÿR ÿ\ PXX`XX`XX`XXXPPXÿdKakadu-v5.2.1ÿ N²ÿ“ÏÂþIº¼´ø1¦nË\íø7g"0µQ) ÉØ ]ëôø$NúŒ„¡¶è™NÇ–^ÖD!û¬AqÒ®En"Ic]½üº©Ï&’®¯qÂî1¬Œ†,ŒËya î…NdѦ'é%(0cwªð [o]y·éõRy·æ.P0àúµ-¯_íÌ5Îmž›My@]Æ%eÂ8hÚ<Ìù©\4ß@tQ‰W^5d…–õTó¹ ÜR‚9b‚‘òz6®Våå•'Á :W“s¯#n Æ8æÜ †ê@Rá-·Øò* `m þÔ6ÛÛýæÇÛL€(t2`þÜ›¦ö›Ø½Ý]ˆí›<"ê$©[ÍÆ„umG`¯þÔºß#t‹Vmø”*¡qàÎ\r±OÓ n’“«œ/4:ÚZ©‘æAÌ*ÐÛ„KÓ"Ì„v7Í5 %5˜‚­¡|rTÔ:x%nXŸ º¢4]qhÐv¥£>1\%mý”%ÛLô´‡©BµÚ ÏÂÐküПXu5‹ a™ÎÃ2?ü[oj <ŠþùisåG1…DìÇ%qÑ„¢›÷Šml9ê0^{ú–§ÔïvsŠw¦€©à½Á„•zB=+îãh·ÓTÞ±ÝS<¼í$JXt°aÐsÌM@}GŒyWÍi×Ûy•Ü55„$¤÷1vRdÄ7ÛË5[ià a”Rý±™'¬¤îíCäwÒç´€âPð®ŒS̓;"fUë›nuùÅ,$àÃÅäøAPÓsj5íÈGÏÃFÀO†fCF€rPðAûG„kÌL¡š‘C¾¨Ür^>HðÛÊæ ëø„¦4Exo©3áõÌ0óƒ±òF5°’nÕéï4TKx0]¹ lͯ¯3EvÅÓ¨j¥ê¦…I¹Š‘£‡ÁÔ|µù’Ê5l‚ÓáZœÕ[m»à€–LJÒ/Jcá‹ÈÿÇW Ež³"HìÁ£’“ãî~ ¬® k¿ ¸”V:Þ6ÏÎÚ½‡áe€aW& 7‰StàÚý®S?ÐÅß™áÒ7¶ž‘)©¤Uõh. èዬYÈh´Ëø¢×PÏšµ#ÅC.ˆ ¦/¹ûŽá+|ûgóe‰Ô>cØú%UTu  ¶šöGóSÙÿ0zohȹÇÛžå ûGÏ5}Ì|~!ŽŠUR'D1ˆ?щ4Vn^%ðýÔ{Ê|À·8™Q5TüT¹Ç¢².+7þ)ãŒÿ š–A’7W£‡(4 Ö4¢p•F÷èeòç‹cBtÐ1̬+Qrƒ’ØVVlUë(”ñõQt—;©V¦!`¨ÔÃô55+š¥)£Å]Ÿ#yôgWÉÕ®GFD*ÛKL´ÐÌ€+VBU‹PH/eiù9j‘¥0R@ u®Ý7ƒEz‰’G?Œ‡“WÊd`+CŠÏüЮ>°597FË‹Í(•$âÅÍ{M'—wíÝ#Õõö0b=l§^Òôê½þ|‹^¥Ï3@2-Êï£èJ(m …xu~ix&…Eí")ËÊ­ÑÒ‚3‰Ž¶ýñ‡^›ßÖâ„ÑJÚH2ð:‚y¢ï⮽?qúJ$q) ~öݲï›g4º{s…6k8{Øuù"âTÖ×½gÆzrãeè4U8=OËøNuDæž>FT›Û1‚ÿyhjVW“°z•‡•›ÂÇImšS‰Eƒt`dN¯KéFsë_Ì”¯‹JSÜ=˜W×}7׿ÐèÓËêÅ òò×܆ßJoá:²‰ =ûVïÔâÐðèRt3‰CÀÓ6|»¶5åšcÎÜ&N`+`|¨KÃí¡aö¾AöÐÀ‹°£ÿHüÌ©üQŒÖißfµ7ŸM¼4xäBøè³èžœò„aÍ~ª',«X׋h²Á\IG:A(Ž*ÊŸõ }«¿g­Ž]ä`ÈÂeIŒ¡jÜê1¶iÁŸ$9WÖe5¬Œe†Ì +³Ä‚FŸ\DpéAPzüÓ˜YÓ/$~凮pi ɞӦм=̵þgÍýMãËÏ h)z0^Éè0 œY<¹›jÚÚ`³1õ _µg"ÍP¶Yn.[äÚ ¨}©Šò’çi¦õ‰ý 9çh¤àÊ@»â'‰ük[åe=ƒ£t²¤‹BØ.§¥.ÈdÇá_ÇáXCð±¨TI¹ÞÈeÅ;G¬(C0ìâÀjdh`1¤êY˜€&E¬OŒ=q»ãŠ×ÓËâÉ|‹„ˆ_—»l“±>Š½Ê q¯í ›s=’Êû™K:ù.M¶)hÌC)Û5&ËÇ©¹1¯å¹,òòué!Žh*µP`/YPPÙ/N]²•3_²Nz7¨ŸÝpp¹¨„tëç=³Hn•æs›i;¥qkUþTd)˜5Q0‚›BÙCŽxõƒêábÐÁ­Ci ŸÆIpÑ.o9P­‡.Ýk®NíºÛH(?Æ4e"Ji¾¬2dÝ"}ÇÎফ¦ÏËlS¬gõ~SF2ÍZM=¶ÝcBÈ­_ºNÂ%<‘–‹€wb[ïð¼’'Þéãù’·¹õœ8z7]Ùž³|\ãª\šúœ¸}¤Ky*a3ÃÙésöP¸3Ïó˜åÞñFF6ü Wsײ{=Úa'¹Gÿ`ç=íç·§Ej¼Þê‚ÏÏ@ççxGç~€‹¬°5r÷=Tæ¨ãç«PSÃJàýc7gø¡DÆÐ¼–Í­=Æ=“>ˆ¯ùx—*jq}úeGGEˆ}f¦ ².i…“]#ÛÎò æZ¤X¿˜¢jAT@ˆ%uÜ õ©Q q:òŽrH•.*:G5xu°³2!?†ïGy·‹“.D|iŒGÑa¨ÜrÚºàñR …±AQBL_JŸy~ëBêÃ| ¸ÏWc±qþ3M )d¶ÇÀ*ÃÄ\vwãß~€ÕäÚ¸è(kb—Bµg³1?.íÙªŠäÞ:˜7@!¹Á˜_\pþ™;¬ûÞ¼4œ¯*\›‘'J.§¥ j±TÒ+Gïž°}DÏœ YÐ+±ýdÉ….f¨“ÖH|Px_c“…i`K•NWiCœ—Nê=á¸h¹{‹DÙLç&Î&C—ÂëRÛz€?¯u=›Û{üå4>æ³þ»™»¥5´¿É7©és^†üÇܤOÏè|û‡€tê>#Ùð2²ü,›ûI?÷hó3$ŽVšÄŽCž‡ÇáÒdüú ‡á¿`ÅÓ ¸""ØŒ9¸´*™x“ÌA3=urOe ‚Sγ; ¾…óúR!½ôÆ)DÄ¡w¦x­–4Åp0ôÏ]š–jþ>׿&¦e%ó˪MÒÝài¥2~¤ë`PÕÄpª¤B(¿³ŽûzÚ¨úÉ>ÙÐM4É4ÎÔjq7É ‰Ï…*˜Æ²Êy .>‹™`Òá0jÈt3þ@g9ƒ: ¯96üXz¯¼ž† í/€á.N‰{z·²÷ r¶×sŸÂÛ™÷Ý÷~öתŸÍZðŠ‚ 8Øo1¿–ádX‚.?ü¤(ÁƒÚz5ÕÀLÍ¥ ¼tù;6wb´RýøÈAx*\YK‹Ý‰ofÛøàɫէ¶p'DTÏ÷6È*º¤ÜûÒ‹ƒèE!Mðhf²¿§ÉøBûκ}#aâEú SÍ\÷XTgÙÄ&‡¯&‡}3’íˆæPÐ µ ×[&Åô“n9 jãaPÅúìTBýïI­1œ–\¯/•©îºC¤Éã\ê©op„ØŸËTV%âܲÊuILØ$P(¬#€)X1PaíÚ䀅M3×Užþ>šÈÐ×Ç«åYý:O[L[äPCEÉi”ñp1™ÀÈÌà(à³+Þмà(¦¤¡póc]ŠW•ˆÖv}ÅæSä]•1Aß0ä¿ló "[Ý´n`<M§dªØ> ÿA‹m’£,M:С±Î¯·h°ÚöyG¼p ‡BMß¾~÷ÞC¡”µÉÀ6ZXÎMvhS5¬YT¿ M[ÏÎ^M  Üõ€Ò¬¡c,ðyÆâ,÷zÚ2'øl1”˜^|*›; —ö'bô"ãLD#Sò•ðŽF~¬Ùœ:vfPæë8òY„ŸšG»•mùáì‚Ï8l0Ð16¸ªµw’q*˜m€>£Æ¸ÚrÍ®áêuYt»±·¡Ï¸ú¶çLM5Ñ€]y©´<XË )1 %Ø™>«ì“À Í“3Y"R·NP…‘ôÓ±AÀ¦ùêT%­êêlz¥Í“kÓA3à,ò…%`¬ƒ"œ·`:Q¹m–~ÉŽŒeñåìœÒlYógºk$¹ŸiËYuWT&ñl“ ” ±ZéLæ6ï˪‘׿»\£`;¶r =BúˆrEÐÔw(%?e¡Ÿî¥1#’ðûÎ Ë3ý° =uÎð‡zhv…î[&à³UM» G–'òÜ0O\ØÐü´¹ì¸p?ù-z‰jÝ–úRŒ65Š`N'ˆØ_¿Nø&|¬²‰>¤½cGÅ:¿îL8I«w¨Éìÿƒm˜,ž‚ø¦(Úˆ„ pµ¿‰2wºåí¸x¯ ñÎ[åØ*iG„À? 2tÕ+¬vq£È,Têû4»þai9¸ 6v‚>Ødc~ýñ¼‚ñ‚ÉGÿa†G β;6ƒÏ›ÄÕмq[õÐöÇ2Gà\>ÙÝs‹ñÝ.é;ç>ð\ì©™ë«w9®dÒ’ïÌ÷¨Ïæ/ŠyPcMÓàÛÛ¦ÈlÛÕ訿\¾sÍs'D„Sz/Ø_­"ÊèÀÁ rúÒ¾þ†ä©ïYúìEsÄíèä¡= Öј 3wfx‘DYzwÛÿEõÇVŸ‡l!PL@3vM^ÎôÜ=·¿'„ÃújëOAnR˜šÐ!åáhAo†r?Ã/©-¦/öì-Ú?“CdÃíÔx}ºmÛ¨`ãUèž„Ã,°[†¨vy‹6XGî$s1çÍb«PÆ~ùìÝ“–V Z¸nØPßü|@ÊÛ¸nŒOÇaõ'ZoÌl8éW%9«vHȵ ³ ÆD‡aFOçÆßʬÆìÞ¾”Í™³[\ã%šV^V’ºmÍ™ðÛ{/‰Ï¬+ø¼&H‚W¨8¼`«};‚²F‘}˜½0:ë¶âFÎÅoê·œRa›‡ÅHÔû`4…õ£IÚT=úæ]#ìk늄«bݰò—ÂØ ¸nšÊ,hk“ŽAø?L‘Ý‘`¿2NùþûÉg-y»á<7´KΡ"£\:™Ä㌠W&ðTM—AR¿´3ñÎßp…ûÅTpŸØås¹jÔØAêF!ÛUï ¹Ä—†qÌ €’$BÔãÁ{·ÏÁQ.–Šu!ã€(NÛbš¤ýfÖŇ…ÍÝCe‡b;¬W¸»ñ=¯Ù£móa£^îõyàekœ«pÂÑô,}.25 [AN®››áÃAÕdtëfþIÕ p|{YæúcºµÑRäõSd•Ç&Bž—òC¾Íe.šì'g•™#˜kž4C i“ÅL“(O‚aÄßìÒŒànèþd̺–¢·´{e³Ç Ôdr©üœ(ïzñ+"³^hÑv@qsXù_idêiûv6è"ÎxQåß¿àUê~øáŠuB£¨Ïî2aÈjÊÁpÄpa|¸xtX¼Çïö[Ñ]5«X‚]»×#v6Šœ³Í¸½¥A¢…N1y1sç5¡êOˆ 6“FN fiÆ ÙÆë.ÛŽF¬}¹Ž¼Ó/ýݨ 4î}ÙD×r›Î?t‚+õ€ˆ2&š!×ùYîÿV/!éˆûü„f• 5Ùè£Àè‡x2ëÏO °@çåÈaËžû¤Îw”P¢Ç–ù°ÁŠÝgËb·¼øðÛrLÏI±—õzvMˆ¾Ûq’OÚz+×Ðwot[É-åÅWÆHÚ­DŸ@šXÚ ÚöE‡©Šæ"ûú-Bew¼rðÞÅÌ諬ïÀ{DúNNÿcNBöˆÿiš]BŽª@mðïïF¾íå•Ýp>D²^0^Il-r˜×¬,ür(“$“ï­µ¤ˆÌH«"m#â þ¸»-ØÂFfLúÄ`÷¿ÆwÓu³øxúînlf)þzÖ{¬­€•%­“ÒQ~Ü$°ûà;ÅŽ°ÊV¾&—¢êZ”pð1£{ÚÇá½Ñønt? àã‡Æm)±0§uÈ|©+M“ÆÙò¶íßÞ¬§‰ÿ/¶ÊC»íÐ)$Êèw—¶ÏËœÒ976~¢±Äv©·l#ùU…eáØWv{tís­¹•¼»â*HºÉ›ò潇ò²´*¨Aø8E€ÖcObkf§‘ti£WÇ»µâ5ð>.”àXµžio¤´$zü(('' w,z_`÷ d¶ž ôÂéèK²#RW ¢²ÀMxöNÇ Ð¥øt€L÷Yõ‰‘±øÜ݋꾷tÆCTÑfW4°ã‚ͳ¸á¿ûTð ÿ$p H{éãÈãg§÷DÂ5œu@³1¾ƒÿ>¤È€“wXEÏÇž®hþe¡=ŽI¥DU@û¤;ù~.Lþr²BMœ|GËu>fã›+=pQŠuÎT"f¸nµx„üXé>¸ÝuUܾ‡n b™3™ø’ f†>ˆzvì7!‚I ¶OòtCÕa«00&Ñ£\äeF¸rC‹ R÷Š%­ØeÔ¹+aÕcœH÷o³Ùe|Ä XZì˽ڵJhVnr^ÆMûiMNGTxŒM¤EM¥%BíËØ$cÉhÓŸ˜ý_8ËËjJn݉gÎåRóþ&è¤\ƒcÔ;k:tàÜ«ÄO|QAJ¸‘¦Š¦Fþ?ÙƒÐb@¯ýVœ›ßlÌ·®²¹oÝ›uQk|U†;?çÞ!o‘ ý‹Õm>¶‚ÑûÑÆ«À$Õ㾇ŠsÛŽ„…ÛZ¶²`ñ—rïñšÀ8µ=ˆ_Ì#‡@ߨ`Ì•kñ©BEÁ¥fŠá‚…üóŸ=+[ dpj;/Q2jó˜ó!þ©þq´wÂbÙÚi“¼‚ŠAÒ”Mª*Ž3ê6@Ÿò©žó§@•óXªçæ{yCÊ /I`ÿOa2føDËé±_9¸®oîŸévxká‘gbÒ ß*²bPõqþ2“‚‚ ¸9 âUD7’Q¶ Ïo:ß{(í[s×Ïb'¿ÈÀ³Þß&ˆDÁºj¶#Õ}µ×¯³&‘½\´*X»+¯Tuíú n€©+š-‡”îÆ5þe9Ôg©ÖŽÅÕ–OtÜa€âVä¼L¯WèñFÌO\̼dS(é;YÌ[3!#ˆíR˜“~Æìu³,£ä¡ÞTÎñ-i±8”]@Þ 9OõdãÜùˆK=®Ln+q®«CãÍ«˜4*øUt‰Uá­kœµ•Ò—yâÚØàî*øãå)>ØHh§=G@­5e‚w뺫 ôNà—ü)Öe< #¿Ú‹mV±nØGÙ¸ ·¦ø(ÝÖ·aÚqÎäîýƒVå+ÕgÕ>ÎHˆf:,¯°¬ËøkÊ> ˆ'çg^,Vèå ÅÕ"SŸx—U׃Òà4—Yª÷Ÿ”ñ8̯4n§/ÚŸáÛ©¼J¾x„HJ€\!=]öÏa&o> WÊôyÚv¡ÛË4,²t¾.£]x§þeiSŸRY«ˆ¿à!…¦@µã¿sÇeÊü’œJÞ!Oá²d[„/ë´[d·ÊÜN;Òr«Â ü;ܽ=´Î WÖ²¦ŽMêˆà}ßù¸¬)hf´¢ÐÑr:z¬¬Û®áû$ÅÒ=r@¸.Nš@‘ÏègJ„¯³Z‡ìmeRæ öìOäS¶©¶Ã¹V ²‡Ð¥5¯ÝÌØ‚Úmh=ÿq³·… µmU‡µ±në %ÙŠ¯1CP sY÷Ž"²”Np  VTi`Â(Êçx¼©H´îùêÖ¥<•Xˆa$ûº¶Àaò8‡²âÑì4ÀoÉò¿ÙãÇJ?{Š•ñú ½ÞGûŽ' à6Lcì¾Ý9¹Õ›ÞÏÏ­¸ü:ϩ㊥à£IƬÂÖœRO®µRPŽ]’e ^ˆ=9ÿ ç§h ‘NÎÖ~Úm¥˜ä{ š²ÀvLâð‹1´—&š£OV¤=Uo§Í0V~žpµôhdsJÒ",X€QŸû¯»¹»q´z%GËa`¬¿l„©òW¢Iv¯7FÃ=•cóæâí,e’zvE5wžÏÇ:ÿ-Båš\pdþîçùAÚEºj9R\§nÖA耞²ãÃ}Õ;sä“”vÑéòéïgˆK]µÓŸäËÆwt_k‰{£fòÞÜÍ¿àX/Äu#6FÌ´Í)‰x3TÔP[^cìVʯ)èÂêP…ýoê=0ñ'¹¬ñˆßùùrÜ&*Š£Ùþ@ÃK'œÚÞ«a Þû½ÌؼC‰Ê 9K-œ€³¸ÇðŸPJúQ¨&MäµfGóˬÓ$Ö3ÌyÌ5®¶ÑeáP:õf¬U©^Ö$Yعõ›n—)§<€ÓÞ). _÷Ê¿u(iZ§×ÁŽåf«Ð×›©´à8[„;˜N¾½®ÎiúÈ•”ú²Ó­;ZÆ£Þ/iqUD ú²Ÿ˜³ùÑ,w€0¥w/Ïõ»ÏE]´Úœ;&SLIªÅët\X®.dr&à/÷Cúj(Ôxæ1\SíÊK²àeˆ}Ö¹ö⟟ÓÚ U"%{ä"ÿ×0ì ø—œÛ‘‹êÄ$‰äûÞd½÷#öþçˆì49å)šyj³I6>6OíÞŒi"¯¾´Z¬U£­¦å3“:ÀÐ ¥wÍ5þ0Í ™¨z/ÉÁ£Î˜Š+$v¢és’®2¾ˆªÔ`¯bnxÙà&Ù¹%K;&]&\˜öt¨Õöª‹P“‡zœºã¼®"ÜÑ´Îúø µXL›ßäuñÃ7jã2?™%6»0…jPð´~©ÅÊÉ$_k°>FVΩ‹*’c+2pmÔBóé²¼À´BBoW–ª)æ_îñŽQ`¯X9&~„c2pØßjÑ›‡\y¯Ðd®·C¾ I­‘ð1ÛrñðíÃU‰ icZú•z‹2Ý8Z—ûZ°³„cä´añ"¤±øk,i!æÈNµ# t¢dÙû…Þ™D"ÄB/­}ÖÉc¼MÆ|¿,I8u­m¤üL+ºÄóµ»K…t@RTþkÄÊd\Þå ϼMôÎ’×mHôh[&¾ÈPg»L^Qx¬”}â³H#§‹h WlT²q¹ï¢"_ÒF}6ª)9Â/­ö<fxèO÷&È1‚®|Ÿì $çˆ=Ы;)ª …ç§Ñ÷!“BØMË«árĈ•r“íÝ™¸¨ž*èô$RÝ~¢ËP­Äî½²ƒAEb>˜ -ÐR÷·êá<e`§©cê$ ý'½ãæá‡ƒ­­Ì¯¤‘ÀÐñ*©hý õÃþ@|…ôÎT,ÌÏÊ–ªÒ°.æ×àk m`béù‹!ýŸš»Å!}㙤7¸Ôg<€7¤À¢œ@›—-¢%cÃç×Z®^1 ÞTaÙ‚jÞVS_‘²E‹¿}¢—L‡Åò£ÂýÄìPzN2.¿ø¸%ù;û½ið!ÌäH÷rЦvO=¡‘\8¢a-ý\TYü«Ä"²ü𣴀ûq–ÍçùXïF¥8Âþë'Ù¯)gpF褢äá=™3…·zkÌLœ<Ôv¹' Nó^Ð÷žfxGbE1ðÖ—ŽmiŦ`öm &VÜ*†{šÿf¶£ü"exħŽ-®š8µÜž9ç¦LŠS³:;Zü[Ì9±Oï>…¢²Rý’ƒvNT)ª§eWů v¦ƒI;}º³8¨ 9wÆýÏ(èÃS¨†Ýt ¯´Á+ÅB‡µ¤¥/}@zë\K(>ÔMÔ_£MÛε@Šu°¬ª`Lß`=ȃ“d!P+@%¦qŒJ@³4½Z;‚µU먙 AäS¹aKH½Œ\–­m„#놛ZÅùGÑ›&aíId´.5’ªö÷¦:‘ÏR(yÁ§35ÂðÑ PÖ°¢ló,¼h °»k÷ìþÖô®t¾cR¡_g±2Ûáò6Ð#ñx;û|ë¿CNšóÌ[WBìÉ¿#¦ÍíQ{HºÃ0+Xc³Ââ%ŒíȘþJX`²‚Ã’ÎðSIÊLðXÀóñÈp©›’ùãë Q ô f6öl#ÊÕô5 “fö{³ìÏûñT@# #{½„#~³¦†£×Ñ9‰>ùMÙÚòVÆÏ ãåHu-Í<¦íà€€»7E³PÌú·¤Ð—Á´=_µ£ÑÚ2´²×b’awl-ªIÏé˜Ý‘ù(JÔ’ºíÜë¹ëë¼'#™’¯ØS§Ðw®uf×qsæjÏõü[ÂQR~wîÃ6ªƒà¤Úêú7„ØØu±ûƒÃJ$½ ­•V£i†IÄtg5÷N‚ßÖ‹ˆ ò”ͺ‚zÝÖl©>!ÈÅo/ÙBÍ«&¸pêõ.gÂÇáêE‡£Ì?Cƒ1ÿt@?ɼ»ÑúÀž`Ÿˆ'Ç4 Ö¿±+3YùE4(èýýd@çÑDT0 é• £8×ëÄœ(†Q%÷’¯tÛ +4“M[TˆŒTŽ “~R; Â@TÔ¹PýÅ£D¤ôªìùšÞr®Ÿ ÄUãüC÷^ïÀªóo7Ó-úá6¶¢Ù©T68*¢ªÌÇ'á/© AlòYžr±Nƒ%i“þ«"sÑ£n/Ǧ%$U¸O-à°WílLlËýN‡xyI,ì_sÊå’öêsÉV’â‚}c-©û+ÇCRÞ€]{#¿Ñ^VQÎ6pq‚¿ú•cX?¾Ùgõ(‹ÊÀ!~¥pèiXߺϳ…÷!ÇΣ€b6‡ª ¿©¯–Xò›»Ç鎱€Ý³(‡Kû%‚Ç ïæqS$¸Š1¬d ®!SÙœðeQ¤pÈûEëF|%á÷êâÛ«›æf–úõ¿-œ(âüÂA½¯oxÝ P¦RáÍšÀ°!28Jï±ÉVÐÉÓ‡ZMßF™¬Í9¬Ë‹xµËZª|³UkV¢}åÅWÖ–„ ·ËG`êa¬±¥`´¯EœÆABù0—‰öQ Ê®ûEzU¬@½ÎiØ#•™aàÚr¿üy[4”xø6Ìý¨å(Ì «Á'>Zp2ºÉ ¼\¦RãeOf,OUyNXâŸL,«¯Û\¨yƒ8o!ßi¡ÓmÒCÈ(ÜI5·R”PU>÷†ZXM©_á;»ÙT BȪ[îüoLòUñ}‚_ªx®Dø_žª€Ű"„ráPììš5ÎüÄ;41 ç³èòjÆòW’ùöʹ¶l´¤ñÆ!Åè~ê,QeI­1«; Läÿ`ä¥YÞ·=ôhÉ{è>ËŸoù°a8й+l°Ýž ^?r§æ¥†`žEW0øq÷ g½¡}ÅZ¼  ;—äˆ÷{IÄ(47eü P\6¨´¾(j(å‰\um%=˜.pt5`T9¡~ì+øbŠz’$ºœµìxBðDu?‰c³¨$KY`Ìçµ™ý†Á3\á4†µ„ßs혴$ì+cÖÁ^˜Ì„Å@ú–¯“» ¹÷…êíä=¶g"r¼'Ds±>9§mòP¨ Asa"æ‘ÿq˜?œë¿I=)æÂì¹I™¥2äå‘ ¹ä(¢éEó*ƒ¡úÛÚâðFÃÈp›¸ Tök¤z‚•'Ùb•½„<»^€w–uWÇ ÔÙ%O³gò+¸K§,÷Úá ÷0Ð$»ŠSÑ-«pU%0gÃÁÚ•†ßé2eèl ]„Ü<öŒ9¤gÑMÁ?ëû0§òh„l({äYN ³, ’¬Q/àëüÎ š[ë¨Ûq\*`yŽÓàùÕò:ë,Lu6Ë»—‘Kìg-$‹Û—u ¢Íâ}ëqs:*7>{ÂÙ5% 2Ðb^`G?”Œªó³MÜÑ݈¹Y xE¾ rå fmïuÚZ”­g hqN.ã§û?LÕ0¢N¤: v¿e‘A"pé-S4ÚÞã$VÛ˜pø;z èÙN0k7%1­åô%úR ô†´ z¥£k驚¦G†h´µòØñ™ I”ðâMªÅÊ«š³Í›"ßè‰Þ£^Ö?¨~zV Ä`=±–§ø­È¸ø†Â… ãünwßÝ®Œ%™&’ñýÇVB OÙ–ž}?}¼H¼+Ä,ž§³t‡Á€+.¨àäà—(œÛÚWE¶Ãr! ;)¹Ò;L²+-lѲ<ħ v³I†sTmþ«ޘ֓Ƈ(¦»/c¯Ã½¶ªk"¸Æv¸¯¡ØÌ-„¹øø· Ĭ‘‚ArÚ¨¬¥j'¢3°r£ Ô²;%#i«ƒ_üèâqR€Ës¢ ³lUs‹™C³…?òû JR6˜ï%ÞU áÏA¥‚—–¾~éK$§‘¨³Ý‰c“VR®Á’Ÿ:û?QÉ;ð%$üIG¾7~¬óoœ¸­èŒ pjÅåÓ¯ª$Òã s4$¤}ënÇ¿O&-lט¢¶Hìž±Ëþ性GV±³Ú_N[Üìƒ0SÅ*Ð<Ë‚]­ÝD=À]BCFgquÐÏõ8ÿ9Àx¿%ò*Gà¢Õ¤}ù‚È£¸ˆ×˜ÂE'Ó¨S‡RI<{0"O½«ß¡uþ¹Øb™j¥"L#zÍÈߨ°¹<î(~f}\@[à’Oô:ZÈå]Ã3Ü GÅ.¤÷jE‰\ 6!“©·°ç­8?®ãC4„ArÇWX¹]ãȨߜ¡ùï±?¼Àž‘|e5;t_á›×…½Iè·g™§]ÞA˜Û–e G‚Ÿi8šXTy¸'ˆ„Ïb’žJ¬Lž“ž­,õ‘@¸DÖce›]­ç°´‘ˆºgî$R¢hé®VC¨z O”U£Â‚Æ?ZE&ïÞÌ{„íbNZ-!Ü]2ò+¯¼ ×'ì¨[-øž3ÑÊÞß!oø –{Ï\» ¼P2yÊÔØ®aéb2gå•‹”<´Ñ(í-î’Ç»— VYI ø¥½ã]Z]±‚¾ø»³ýaôë^'¿ö5`p‚°ðéŒÈj&™”4µ«DDU×Þlº/µá=å, eÏ€\KºnêËúáÕJk` zÓÆDX"jkíQ´%)‘Àì‘xBKCs´„Ý“Pû¥…B‰R› etùOâ%>M¦¨ßw]-[Q×U/ vRÒÒÿf‹ÅŸbÉ’;ÔÔ;búº’B-sß]GPËäÿ Ã(c–ŠÂ©2ñNå0 à]<ËK ]6cSÅ ª5~Çû+ˆIçpK'é9x9½ßBÈüð#ÐWzq‚öü6{±EÇÆœŠóQ~`¦ëGFõRA´&ñÜûõ?ë55Ä¥?—îjV:gdqò”úëšžÖFU3É5äÎ…¸9ØÇÙ'…‘ÅfÌ99ïúNØC’<ÇšX¼ … ¹9@dÞHmJtD– 4\È’hÏ»à†+Õ8?6£ªÓ þ>\úÃ&³$¾ÑîÌð5Ü’¾ê§­[rvÖlÖâæI¦ÄÕrs+‡ÜþV~s]ÎÔ.K©ß/~”ø Ÿf«]ä¦ð?dæØfúóTáÚF ýñ-¯]©ìòªQºVËIÚ©2ðù·Ëך¶Ï•#uÄ,˜¼¿¤Çáˆ6kd{ÿ„ Qð¥è(pŽ'òbp~ <<µ<ß…™‡J燎X=¨Èß}˵k÷güè:'¤Ó£5×”:=S³f~U a%OV‘œ<”Д"€¢É¶ø‡¸ÙŸ.&^ð '¢WŒ‚ÿ~ã{yr áÚÕ­¾_»^Å­õS¨jV„k™:)=s¡× ôå¸Q•½‡tƒâoæFZ&‡¾[±·t¿Š’ÂühúTvîx5°f>‚ƒìÿX3Ðf¥ó‚a¼v _ ƒ¿ÑýµÕšÚ‰cÔ’Êè¯úP•û^ﺒ—¹cû\ÿ3€¼p`Uⲓ¾¯Ç˜œnõS³ª‡DË´ØÉ×_IèêJDdÐ)¡Éæ®ÓðÍU£µ9Û¨yÏ®ˆCqŽèìš¾jïU®mÃNаÛAROÝ[Ë(À™¦}½¾(!ºßÌjH¾Œ~ð­i…bï ÑãÍ›½¶$Œ½º¥£¦ˆºôšÊ,ZÓÈñ(Éúò’@a¿Hów'Z|'»f\r¦º•v4»ÓÄoœ 6ª¥ïÒ²Û/² *G_x¬õ·Nwa„ïFÔPOe'$#EypT*lT¶[äò¥@-“—ñÉiÐé+yDRú6rjE:ÜQ‹܈²+ FߦÉEm¶ªò7}2é¸cåm:KÙL‘f›tåÃí뫪˜>Þ²Àöãª}[ƒ{rÌ_«Ô4ÞÆ1¶RçÚâq3¨ä Œ¡ºNTNµ ŒÐ€I[–H€ù‡,꡵:µ¥O'˜¨Ôm[ïh73±Yšq5×>BáÒt¨cXá^9/F“)GÇžHël…•ä‰>5Ú0-È-n¡d„PN—QÂ49¶n|Ú¹˜šQÍ;áŒ~®EŠ-½=V¥îÌçOÜHéá‘Eëå2ÈòlNã?À6ãì4?dÛýê²¼®}:„È»´&¼|Æ_wüm[. w•Q•á|¬ÜïO¾RÌ…ïÐXÙ{.Ç ì!Ž“bF ¹£\‘†oÚŒºIRHSΪšXݵŠÁ±‰êR>ï¿­Ôß°“Ý2Àö9”Ì?+¯ƒÑJj‚j·s“áutúºÓM»òmm+j‚Qq´§.¢°"Y†S9©ç>3V“¿gê*Bñèö6×›÷+ÿ)Ån1åe¯žfy}IgïŸZWß /©g¢.fÔ¡H$n>ì±Ù=Áp)m4HùãøSçMipÏ›x;/¹ Çl³åšYßÕÒc[óîXeQŠ\™"•@väv:ü- Î;Ö aG+åû|¯•ô?Á½Æ¬þ=6ŒàÅ{Á¯xèW‚$5ÛT:/ÿKÆÿ/K¥äÝ78OpgÊ¡O¯ÞaX¯©ÓªH¿4õ³)~âõvHÓÉaó_ôIiÙh,û½¤|»Ïn´‹×`XE#ÑñèXiÑ4hI£$˜£õJ¥†#e|HFùÈ•uZÑÆ^Zà‘¼f‚È’êyÀ¦Ç>S€[ûàø·È!ûŽ4q`ÉÌÂùc ñÙOßb²¿'RÉÈιL±[…°ƒ•„Û–výp¶Ë±¬K)¿”ì¡—á~ð/‚ûU%œ§ÈO5’† _¬ñ]ΨéN̼vÖ›¹ò<Ïö³´öò3ï¥U—Ñ;+fˆVT ó'ôŽaœÄÒ]PÛ“m#«ÿ:„ð×ÓkD‚Ï'#6tÜñb®:Æ}ÎÉn €åsÖ”úB'¸Øè^’ÌèpÆÜñ±‚L˜8"jzÀúÂ-ü©ž”ÒÜXjl EÜEÇJèK*Ú:ìùÃ-˜¿/?¸‡“Ž ÎÙeÔ€e÷vl£H;¸É2k'Dé ‚û.½ñ% x0>gu}â°t¤tK5õ³Ï‹ÂkL_2 »²Ú}«ÞOÛÉh¨ÄQ¹»õT#"5Îgì|;?I™ˆ¢ÿ 勨ašÝo+ Ñó­zö|I -Š8–¡'BT±j¨‡sµ.+ÀF­5ó><ܸï-À%j_Nåpµf¼j„ƒtFtù6|œeÏpzíÆüÓàÝ@Gû:eÊÉ Ñ¢‚gÔeÃøÖÅ&·Þˆö¨0gIb™’¯#chÚì²BÃj{/¤O䓸Ìð±nSˆañÑSs½í ¢e¦ïËH—ºÂ&¯ h:dâ~JDÚ¶,ŠØ ‘nôÚ'5·‚úê©¿ËÖ:Ïýý‡bp7Ÿz¸WeœëÀƒ7vö¦.v>9 Ÿ<%þgãVEû]z±m-(0¿&5™«uL0?8?‰}±´é“?.x=vʽëÎ3’…Æ7ÄÕ 0¿-Ø!üšq>[ÚÝ^Þ~¾Jv€‰¹ý= C Uçv’Òû<»s`N ’:Ù&äì^‚;U6³ÄÏt=¸˜QÀÃ}þÌ# Ù¸âýnÛI§‡¼ºþ”Víå(dQ—1FÁÙ©èŒÊ¦s:mUÎûsÇXe*$“ëx’š;E0‚®õlÉ©%ùØ¢¸’±1Ì/Û$Nq’à¬Êè.ò0ÛË6ëiÐõ\hR¥cÖ¢nõÞ@ë(ã½ëú¸…¡ÔlE‚[Kù=ßF…–Š ñêAf4”ÕIöÕµ4Íœ{ eÚÝK¯6ÌQ”òÑŒÜR °3Ñ.([¤QXÔ/){Ün aœˆ°‰«’w|™Ã¡L7:CŒ#EÆèÄÕ(áæ·Ž¢[ϼxu³ñã‚>“‡%šúP—¿ÊÙ°Káço!ü¦ÜšXóÎä<‡a€à‡œ!'^"R‰Ô Ÿ(Ê¡‹J)ÅÙ>­±JtâSÂñR¢²XòNÉîÅîS‡+¢‘iP…•Ñ?ûa,ŽÙÓµ)Öìµ/—¯Ûõdüܵrãbk7)é%Dµ§‘˶hS}Ç«mÕaù°qÞ¹BžÊê°c’ËLNܾ‹bA§ÀTCN^+7‘'6ºÏèZ6ÝzŸþÄ`)8Œ"—sCuÓÒMÖÚ,F£Éj`Áì ¡Û|“ÕdÆK}l_ôkʪ’§nt‹k7-Ì0÷‘Pô±píÇ"ž‰ ŠGÁY~sàÊ=vÜÍlÌ“:ü j“»Fƒ±mûX9RÏ[V_7 øt­Ý]BGLa¬ ß/> AÍÏ—éí±p†ÿ £868p³ÕÛ vvÀ/˜éS º¦n€3Iƒò OÔñÓláÂÅæ€·jŒë)š‡c2nÆbàŠTœdmV³c!ž5Ä"ÎAáþô"ͪ®Í²+–åÃYÇùS&´Çáèi·¿X~‚ øÜ´›Äö#‹„+YA¸Ä‚]>i¡:>D®‚Ó3¾$FX‡=M>È;{H‰„+t‡Žo+œè˜ii.$c6ÊÃ@ôå‰íµSW‡’å/ å?«"Z­¼Q•C‹“ÿO·¬Ôcpo6%íBºã¨ß•eëä’ReQküM¿è•½BéQºlM ëëÌÍé“ßR¶‘ÐÖ3o¯çÜT¯˜ÊÙ:ë;|1ÿr“„«öÒ7uÿFìôÔ ½Sn”Åd®-ä…Åè9X@)G\íYv¢ŽRB”^:…åÍëÈwRÿ[[A­D µ@—Zî&/m]±ˆšL|+=–U#AX3oÈ(¶Rå(Í꟧3òOµÖT&_ya+¶ÙÕ³70HtL÷"¶èE*9lI¥Úƃ¶M˜Ôɉþ±GµõÓ™«E®Ú,è!»N4 l#³f½YÙVjxõ÷‘b…\ü×D•³´’½µtåñÙ~™]Љa± ” àÕwø§Q'MÈïFn,ÇÝ@HiTÛôµ—X &„p¨ÕÎÒf…< ÖA‰x—uŸÀÈøºÂ:Ê­mÉ ÆçÞ0Ùwá6¨±©j%„ÕJö‚l\š›H„'²ùIEanî3¡ Îm¤àƒŸ´!FT›Ä À)ŸwífUO# Ž2Þ¤æÌo¯­Eðã øîÀ»UÎ[ѦƒÏ~ßá½âÍ2"YvSm™'Ëø¦ަá;ïÉ€O´EsvNfçÌAçp„}°¬tT%-[Ûú™Øy'ü"nì1I¢¿I'Lå‹ö“Ë)ù ¼‰5sö&ìÎ'Ú„¶kM”$WP…]Sù_- Ð6LÚb¸£D¸/¬ñæ^÷¡4c6sXHÍtÐäó3úÓ¨°ëÏk7Ê¿ÈЦ#%$þ•öA&’VÂ;,€QjÓ‹…|]ÞÓf @z>»ÞI1z­DðÎ0#8|MÌïzh“bÛª¬l`OJiVÎoUø‹¹×sDéɋÆc´y§{]Xª¶ -£qäZÓ2­8 Zp O@oÉY*ë Å´²,T$ó”imE#FUA»Ž{ÕÇιxùزŀéYŠuý£vÁ¿(cË$¢f€£i®,®\7ŠA1­£­w›ùQäÌ3Â3(à¨o@ e¤’¼|jx ãoE¬CJùæô{¦_âÁÛlWow÷ ”¼ZÞ95xÏà™î¯ë·°‡˜äþ!ÜLýVc1²ü&™i[BQcÐ5šåH½æc‘-Yþ8IÏ „†=ñm¦ƒÇ¾Ô_áÙa{Úµ¡Óläz¢d´·õ#þµH4.‰ÊÑ짯×ß•‚±ìÔÄ9½{J9”xi¸¥­(ç8Äc½=•öƒŒa°Ï?óBVñAX¹1˜ìùÎ! šdñJ$þ4Œæhµbâðýb³œÒ\•g8æ¥à ¨<Þ¶¡!²4²[¬Lõ¹öig I%Aù¹Ûª“%Yh§H„oBº ¹ÿ ãg‹xòC×·Í“ºWf†Á{/ã¸6sŽÉç®Üæi¾sÃį 86Å®_“Ú/Ê cHöºc=©K:=D‚Îr-k¢cvØ®ò9Þ´àÜ/tžwµ÷ !šÆ¤Äé5OfƒHñPa…¸×m É5Ô9FßÖîæ,Ðþµi†²@µ’Ð;wÇ|…l·{13 )h `×)°Suî1Ù «W¡ûä$ÜOFxó ¯(ã÷ì|@ïsÔjêÜn“™¬wâÒ|ȉ\ñF‹ƒ–aTV~Ù‚Àßøq‡ÅàjL&r-ùj’ŠÒþnS Â'L.SR8R4‰=`¯™²„#ÁÏYÞxØìöd+”ؤŒQùM—*Qª‹ ÆÉp«7ªug«¡­y©ï„M½}Î#ذ,å'ºM5Z“š)ûšÓì­»ˆ¾µGU’êÒ2#YË¢õ¯ T´Qb!lr}*žÙúЬÙ_HˆÐçh1g¯A.ìô¡Š’>EÙm"Ò™ÉY¥v œ'CÉ>ºa[ÜqGj)lf§“¹u‡…HQ¦ik5?}êÉ6ÖŒI\†w<GÔÃï‚;\½”‚@>6 5r:»‡DC f–.¬¡ø)ä®É,ø¾{Û Q ñ­ÑØÌ~ï“êúJ$´ .FÛÏý!ô_]¿cÄ4›YmÛì–lƵF²œL–´å‹~™ +¬ª/>iZ{F=þö„~ýço¢ÓÇYfR(Š*¢ù—­è÷gªïjGòËÕTž­R._äv¢ýˆífT¡þ¸üAZ›´Ñ#}]'@ÝÊ¢¾M‚±†Þ©'ÉI@©~¶"9~ÃÓÿ5—<ì0”¸éÔkâp§a8}ÚÚoü‚@»ïáù/H«‡ð¯+%ãô‚™-f˜5–$jv{¨fôTüÄyÌl~¡?ì?Û&!1Œ­Évp÷€Ú¤àN/Q¦S››þI–ƒ½kê<ÛÞŽ2fz¨µ¸ ÔY|t8ºØÉû1vÉ® N}ÒNØZÝ|/¡¼jQ؇¦UD‰)[þ.‘WH¦a•Ÿ¬ªRâAGâŒa¾X¥i{ÊìåÜ”>G¼YßíI6÷hÕ ¸Ë”³J^¿žù8a€4 ÛÈÄ݈úzPdzYFÈÞ$ÑÖ-QŒvF­›£‚z&Üw¬#ímÄw¯cŸ\šQzÓoÔ¤óêò¨s O‹‰n¼jácä.®çÏÍØ³àø#!î¬âû³ÃhiÝßn×J1xŒèßt{¾XO`ò>Z´Ð;?ho‹@sG’¬©^(Ir«K*‰ ³݉BâVI3¨mÔK”ÊâËþñ5jòþ˜Ø,T­Õˆ·ðÔ-7×׋"ßÎ,×JíÖ¸øB¼m£C®õAÄͺu6½bÿD\3Qx­Èû 00Ô5CèoYv¡<ëù9ƒn^:PÖ’X6Ê1¹½×®^õ"ºË2`þC ùø‰5V I>ð“˜xÊ A†fN›—RˆÚfØ °s9/㟄խlÌyм¹îmn^øÜ!NYgŽûlR[ß­\î–1´z6æHK5|еäч‹Éç‹ÙoŸå´Ð1³8í»Ò‹Å¬%’ß¿f|rè ½°É¨ÍáìÍÚ¬%5$Ÿ7XI0ßÁ\߯B=J;¦¬>ùew~!# #8&8!·Ð@þõ)ƒ†3Yž©v4„dÔKÜ6J;iÉÃÃ=ïÖ’þ¡˜ê~ißq°öZv˜d /é)b~;†lS^«ÅëŸ9³Ø¶L؉ßÞ®2Ý?X‚˜;ìάS;ZZ³óQ)¼‰;Æ)µÍžÓ•ÇïäœÃ )}¬È ¨H<üæ9 %¬wµºý2ÐSÖH–³<Öˆa Óúrà«•U—jøÞKæ¯KCCÉ•nÆœt•ðñË<åš“Çv'Úà ³cD%wÖìf~ ôYÖ¸șÿ:ê´ôØÆÊk«¬f¬ž*”˜kâ®û5É ¡ß6qw½ wa&Ê »Ù:Q¶6Î¥m¡k=\óH×±M0®{;—«N,HÀ±àŠ.8¨å`gŸ×òº³LÂKÝÖžRÐîJ€ï||昢zÝ/(õ!ƒ“ªŒZT^GÅÚ[ð¯EÕˆE¶sL:¿>7:%µ”4²×(zÝzj2vÝã×ÕD ˆþ€‚kϾ_ðovœž&Aë)º0íE‹êÒãÄ@¦iÝ»øfo„„›ígÛQÆà aì—‚¼¯º£B3™²Ø*lÞщè \gëE¬ßP[ýS #¹’ç2TnJȲ7Mmˆ0횉'5KéU–Ú0ì²ZÅÎÅ9ƒ,ý;x9]NµªÙÐÎ3Ÿáº2Ÿ®XÍ^»ä §Ä0ÌŠ9Ò[j Õb_Óà6?_aO ]Ê{L6§w6ô(€Ìј+ßh–?Ê…·I‹Ø—5‡g1xÒÊïYwùÓ¼ì% ÒØ¨â‚w¿Vô̶ºG¼©·± 0ì̡נ[èÅÕ­“¸Úu×,¨^ï-çþÁ¸.+gŸ·ßÒÏÏ×ú?WÈüýYÀî‰Jc­’Æ.-½Rí‹Aº Iè*",vé{Ôah‘ÊZb õëF”µ}©ò,RmA„8ñƒš‡õèÊ»~)}9.žYÓ36rè6èª^4^ßõ–¹•v¤±¬Ü*q”{ë aHÓ´¿ÕWÄd.Ú‚AyôÆlkÿ1—]Ò# kh{¢ù™¸]iArGg3/Ú¤cÚ[»™à¾Ã|z ªûAèC_9±‡Ÿ¾ü-2h01\$ÀVqù¤Ù`å3Ü;]q»ç1ŽçV Aù¢€œgr!Ê'Ìä5ê^5šÑÛ(dÝ^³ør®¬Ê[|Ò—ÂÜ–;ó›ñkã§×AH}1t,Zoðàá½_7WÂ[·9¸Ma^ÐãlÀW¸q“².6Lƒ-¦ö »òD⤡,d> 0ú³aprç}½¾fñöêzþ‰šsæ‚/ô¹qôÚt ØÕœf‘²äÏ˶ǹïñý,d¦às‰¼Ëû¢ÒA9_ÕºKF˜(€œŽÙsа Xš¶Ô]|›¼$¼¹ù&(ùµãåPÉ:Ô¥rï‚â)Ø9l±DUdØDG–f‡gõÏå@Y˜ò¯ËÆ>^^ÉCzÄÕ~./oSLv C%Wâ¼ññ7±v÷ÊDäëï±U‹^MºKØqU–:y­ê¾Fp–óç¨äÿ8@ù@·~Û=.ç(œ_/Y»ôªC©vü;Ëgݛܯ{ÔxÏAFäé­¬ƒg.jE!” T‹ýðçº=6Ñ Ó† ÚoUfa;–Öl€õuÐèUTÉ‚ƒ‘¼o¾«¾†X‹{àd Æ È;%J°Öôšœ¨sÇ*œ¦’Ɨʶòþ\ßü iþ³KͨðšT,Í䇉2C ®j=:=„4ñõœ4–jŽòllÃgÎùqBì@1‚¨GÌr‡Àm$ÈoÌH ÝQ;~+ÙP¾‚y*ì[x00¾£Ø²””bÆíø6ý 6°G`ÅähM ½õ=;nÎélž&+Êd§i3ï†ÏÆ9¯ÄE›©mvs/yU+¾¦ã€.wÐ&™Ÿø½Çß6·¶1ShüòG@‚p—lßP aÆØÓøYbfƒ•æšñ¿¸V‚DfjÉCZΨ§)Å“ *çITvW”Ú8hß…ÅóÉŸ€#¢7,M9…¯õXõ&ˆ´åê̈D*‘ÎËwóOˆÚ]ÐÕòôrcj¢l#º¹W‹ž33ÑﶨŸzJLª1Î4fr˜¥~-è5NÀÞ‰]æOÔ rY!ÊGdÔ›f’Éjá”+±óÙè0u†K¯x‹ÂÆý„!¡s›¢4ì{5?ìr<äµ=7\T?ÊQ³£f²¹\Œ‹Çë=éÃZ,Qp«× Zr×ðƒíÜÃo×GK b¶2åÍgÉüfýÆÞÀ]FËó½¾Ú ¤¯V*éñv4PÑX]–k}ŽvÕöû¬ïHjO3Ê+ý@;¹T‘äîÕp?C°ÜµÖ©Oó,˜¡"ÊqŽ6¹íê·í9žäã*ôH^+í¹ìÏHëm•‘M†×iÃ>Å5^^YÇSá‰=ãp³Ó-r<ÊsM³½{…0‚¥Ø6Sª®—þ22˜«ˆÿ0¯m°ôoYÌ`%Ügo2|®ujzŸ"Ï ¤‡·KI7v—vôq2V%Ež­û’7Ž´¿õÈV-¢Ðk÷m6ÙÈXÅH¯4yÄŽkf:Þ¸M롤ÿD=FT<î PXƒ$0Ý)¨|™ˆ2»ë‹ÿVqTY¡¢º”ñGVƒD¤‰T G``Ä¥;¦¶Ø¸€†7º!+ÞT?4¤ Þ•Kì•\zÏK›6-½ñD‘ˆ¨ýâûoŒ¥š_jÔý&"G9ö´7ÖjÃ…L•“„Soá=Mò}ÚÈPª^…9„Ì–:ºýçzEäL½w|ƒ>!™}L„ÖûÝZªŒý9v¥ pÅnŒ\¸:Ø÷’‚¨_OQ»¨íAt~š.ÃeÕh´dœßûú;íBÑ®kÁÇK DZâõ î2gIÊy‘ ñJùâHÈr»Ê5äÁO6¿ 1WZaKOŽ˜ AuÉýâÕs=MçŠ.ny;ÔQê¹E>AŒ‚òª_ç¥Xÿ(-=û££à™›!aë Ù<$ØógσÓõ00õÅó«ÚÎò!úâŽíŸÝ£õ¸²ÚáÒ6âÞE­®>€GäÏpZ95¼ Gz-«*Khßú|¥Ìµ” šHÜü°Ng@ÛKjñ§ž­O†®B*lÞ•ª€PIžëzÕ0¢³ü8èÆì¾Èo‰jJ–âÇ—Ùþ~‘'fnÈ„$“¸'ˆ<C¯ÔSà/†3`»Í4 W’î½@ôsóß½²®ÖdÝ£HAe4ÔtRó´ÛÚPž¦§zØA»à+<çÊÉ +Î}íASÂ:—†‹ç`ö‡Q£BVçû îXÙyÂGÂHŠœ45ùlˆ@ù`ù>°®›Ô9œíCŒ>á¦z â_0gzY¬ DòŒßåZY-Ñ3}Œ@_o˲ê÷.D¢À™úžŒº—,èµ²¡î‘ ¦UbÜ÷Û |³’ŠãÉ2±b B¶DhM٦𖒶 äáƒä¶s.CfmÄþÿ€ý-2†0ucN;¨W«L˜Vön‘âèZ˜iD£',nÒçå^|Cæpî? š¥ej¯_%Cvª úãã:æ¶ ~>Fú#Âh!×KõìE™Sq1ŸÆmE‡ˆ¤`³Áµ“ˆÊ¢dìy¡Ð%Èî@ªÆ J ¾Þ‡òxèâñKU4g¡Î€´Ùã:ñˆR·:0€h§Ë×*,ÞÈÀ‹Hè@Fä¢çäÁ—Rù¼ÐA~ðˆ Ìݪƒcqœ‘¹¿À[8Ïò-ðJéMqõpϧéb>§  ?*"ý ž¼…“¸V¤€])³¢œƒÓ&ÄB€ Ôyáwž)™ĶeìµíÌ)ì¦.£8DMIµÖpÞUC³â‘VÁXªòšÖ§!/ïƒÎᓘý0+$ã±D1£9kmNBñ°Xæ¯rô°¨ö _x:C‡æˆsÑhXåé ‚"꟭а8¬Ù¹µ›±…PÄQu¸Ó,ŸrˆœüA‡4o@‚Ù1$ àƒ_{:à‹J‹»=02âO¬ÊkÖ@LM²›\G «Ã©ÀàHˆS’ߥ$ßDUöþíËÑè¢u ÷5vŒR{ašÑiÒøùìn·$ò:)d6)ß4yËëÖE‡ž¡¥,ºÏÚg Ïü®0™Pó Ý&ªÚz°ë.w‘| üÊÔ9?é [R•uªMwm˜¤üd8Ø¿¥ýU®Þ3S!Äç¤Ô+íËm½·œ@Éû‘dt:UF|™?n¶™Imnµ]ðæ‚¯kÆ÷‹0_bÇC_쇹¦ë¾KñirÏ’,èÝ—á3Eçªï«=~CÂ!ÜHäYAõzQQ¦¡~v\^ºÞ’,ÎSeŠ«7À_Ž:—Å€¼I~Z†lPã “ßœ–þÃ7wÿQ°*å¡adž…[ò\°Lƽ* rXfÍ¡=„Ow› ìTPëÆ¬$—ÖƒÙœ°•œ×ôM^à'VÞXü8t0©RPGÖ 1ÿ1ü!±õî?°L#³„L¿!r(çµÇ†4âËN #ãõø{?*çqc(rÛ˜¤ïE ô'_š\®ðÞPs{KjÚtHÛ ÈN†¿þ+Ä÷R`Vþ þ˜ˆÏÃ:0tØ¢CøÀ¸ÏAnBZþ±©c>丢ނGÒȧõ :%„–Êf‡Çó©|ðTôÅ1ÛøïÞ¡kÛp½ úI^½~.{í…‘" %]ðØhcdDzÚóøK %`*Úó’²hز"”jdîbÞ‰¦¡íY•ƨ…;UŠÿEò*h3í/±ØF‘Ý•‚ ¥B“}ºÀ•ØJa-lìIrUf{oý!(ìZ’É’AÛäÇ>lXêõ£Ë}]þ·©‡µ5^øÄWÝ×T…™p^QˆçˆvË›pßqè_jÞÄüG HÔŽ÷òH½)€·ËÆ½× LžÀ÷ôè ò29)ëWžeÝœôæ2Y½Ê˜ÊºÂôw§œK ¥˵O8ý;ëŽÉ°¹¹ÄÜX…|G¾’Ìóåh°ßÈ«ŽË¡LoÐ1~ÛaóÅï"w‡šžÿð½ðjß&‡¯ØCÑÁÙ?/ToÕ2…£J(xÈBØÇ¯§Í¼Y= y[âVÊn{Àk™l¼÷éU¯ŽÑ e)©Ðbºü D0t‰DŠƾp/ÎÈ AoÉŸ¬é„‰©y¶ž«µ+L«.-¤7¥up'vÑ¥x2n¶â&¨Œ^C©Çì{àœª½Ëé6Õ+ކMTZ•‚ìùw@¹`ÀáR ûÕ“Ï‚ý]âP±é‰úh¤gM3ƒ:NÑåPCþ ¤`qSù2õñ¤žrgÁ@:QËNHNv3Ã"ªtšo”C˜W¤ß=<¢PúHhme†Š+æpïÕdŠT¾ÿTLy3· Eð:ftCo\ãgÂÛ/”.a@}(¶ˆ(íªÄ ã|uŸôÄoÊ0 úß–#hHÉ_ö0çÒ¾&{ÏÜâ\CƒSfp¤C2àù©FúiÑþ0¨0Õo†g=L³¦÷M²—HÖÛD`ªx1 rª#d ¬¼ 8I‘æ*õÓFyÓ¸÷ߨ[ä…{e‡ѦÇìóóbYsÊ…Sù zî¥úźcæÉ>ÙÊ¢Ax@~B¥+\Í¢ äƒá0áæ<ë.?] ˜[ãl'¥70匡^ ëTXhüIÞ%r[´¹N‘3#kvôúÞþ¨mЦ§|“PÁ²1öÍ+Ö[ÒÌ*[f1|àÏ?QS¯o„I}Ú(õ¦Y’  TUÐé¯ò1¨Ji´#Ö1ÜŠã¶Øa™h[†T YÃÐ<×¥Ù4I€ñÃÂ鼸QKBÀn^5&ÜœÞÇmZº1SHãøuü:.þÖ±ü:SþÿóÈpý½þÝÆý»oûzÔöõi®¾ÆgÿDŸám,À{ "Ò-Pábâ' '!·µî¦L´ÏX£¹ E½Z».\Ôè,€Àþë~?1Q5²ÊhèŽÖ¤¨ùÄŠØ‹:À-K¥­ÝQž$ÜùxµÔ){¸D2 ‚l]o°¼Y÷‡‰TÒ­ó!2a+ÐZ\¬Ïn¾‡Y47Üó-KçÀRt:,3wÛ õlÌgÜž{—–¯FCuMZ£í]ñânô dÕŸŒ5+˜`¼Ev‚ÎÅqxÃâhK×—R#×}×ý^¡]föÅ|}|µØáZ™zÁó‡ûíWM>¨¦5cb§#ë¶f»çP‚™-Ùœn¯ðUI%Ö*U!=‰OS õ¬žUÖYË%±ÕÖsŠëõz ­pañ­5¯À”\Ø;hϓؚl'N©÷‚Ïœª“Œqè•Q&l±ËlX £Áh¼©2'à*B =½´UíÒ£ Ôßéß&]ðkùb]¨O]±‚ìb(ËÔPÅ 8áÈzצb²“L:k9…—<†“žõFb·›1» ¿ÏÝMx›Êó®z_Fœº–I&ç–'–[’l†¸§mÅé)£ì¼Á!û@ÏÒÙjqYï•x„Ћ‹9ìWü3»Õ^WÇIà ZT \h„‰—£—6í›sC¯/ôê,ÌGVÎ*ñD±æ‰\²à0Oê,ö| îEŽ»çHúýÏMOYQ³KG’ÒüqX`†íÚÀ Ñ À^áͪA¿³Ìoà5%«£ßR\¥Ü—ÜZ>•çµ@øq‡ú™° KÂÈ“!|HÁ*ÛϾ\3ƒ•E'Ü£‡hºÍˆê¡¨û„ñ0\Sê IÝ.šD¥¢•M5œµáUÄ%BÁЄÖò~‰ËàŸ^B¹å‘x…rÂÑô›«)Ì…¤8ô@ê‚Cv~ÆãôÆe¡ÀÄì_wÆ~ÇqÀ²ü¡±÷^”žë‡çkl{òuõYºÆ4îØÞ‚¾0¯2¤«xÈ$‡&óújòž£rRœ’¡å3–ØT,¼ÅY<¹9Å»šÎܹGdÌ›3îjÇË´Æ`½¢,ŒvèãFY»D‡œM24ïÔüÆôÂwyküCU« ÚÝ®Oc7Ćؠñöe¦Þiޏ<µ[³6·‡ì tš*†‚ öØf’wyŠÏ·À:ˆ„€«†ãÿb}ÉÇïNéð÷ûv²Ú´ßM‡³Û(Ž=Jè—vÚê!:ð‹—½;}ÈŠB5ûbÉÑ·¯ ;?À{­Ñ#– ^[TË1Åšˆ eq:p7å¯,-Ÿ’ÑÍ?‰zLúdQJ*‡;ƒMm ñ"0‚@˜ ¨Éô&¿rÛýeÌá¡'MšµU)?±û¥°®ýîâR{‘òµÒxŽoÑ,fÆGuI•0$QÎéénzD7¥Z|š¾þç´—Vÿux–ƒÓ›Q’lÌö`íl)oÿ‹¹þ™®xÖQ©êe$ä°f_ÎFäuÀ»à˜[`‰­F¡Ö¬g¸È¦€ÕîeLŽ ! Ë&ÚÂêtòßã-G%0’ɣȲ§ ÌDïz¥¢¼K•ÉÖþúÃÜ$ Oà§ê^—ê†He¸.onÏø2±ØHŠÏàu‹VYÆ^Èú8¥IÑ­º*a½ÍÐ-a@$Ö ÜnáØý°!2Ö‹¼–±“^YÜÎÿmç2ÞÁfZ÷øØ2yÆžhù[0AqLòŸm½Ê>yOtJ¶Rãÿ;®`šÖóÔë1´LEØUi:~±çºCæ·]É]²ùýÌK™7ÞÀ*A>¹¨Gµ£ø ¡"©&bw¸ qæþ̱!ñö&¸:™£–CÀ{ŽûŽÂVaÑÛ÷ 1º°;@[ï®ô·¥:í¨ƒ$&õÖ`âjÄCÏÑÏúù¬sº9ŽY^»~-©×øðƒ¬ª¶ä1‘ˆÿ)£mUeA­—ó ¾SÈŽüÖ²Ë5Äí%G ëzÂ7õ±S¦MR0CNîó£Œ:F(ï • nšZsö+x{ûv¡4âˆÈÒCV÷éñµ[1§/íx³r']üŸc‘`m' œÎeIÊBýƒýÏz»èÿ8qߑʄ€®r˜ÌÔ{ª"9Ÿ€÷HÔåÇ ïN¤¡.Ѿ‡?&õRA‡ûŸ#{…ëÄêXÙ© xfÊ3Ö‡ãbmôà‚â60ËÔ0äª:ÿM¿XÝwA[+8 €PO ¸kìQ%KÕç¡­Id6 Œd{ÿc8‰þMGñŽ1¡ˆL™dl°€JJ,þg)ä4ý€ l5é§Ò$W$ ó”c[Âÿb]*’hQ¹H·/ö™M'ÅòÏ 2QTÙðÊõÑ!œIc\aòÃ_8„Û:¹EäÓH,+ƒ³z» 7µè®ùx]ç¾¼-AÛÑkÚ_ùÖ|ÇØÁ•Q»ª3õ‡Bžbó½ö³¹ÀÕÏ×ø¸{“÷X76U -õ~d«±·>N³"›x;}{UÕVÅ^jlÓSª—/kjÖN‹,Ç¿5û ‚'˜ºN|Ÿ!B>S xåæ¼Ô¸ÿs0F –ø°ã'“×CQÑghy?ÿkš½WªN¬2–»®,ô*¤œ¶L™ž‚ƒˆ?/ªÞkZm§AsdùÓOÙàÑxµÀ•¯îl6ý횸‚”ЋT4÷A”¾Ï¶Ô~`h—}ü¸šÑâòý”R([Yñ›º-…_>Ðçgúa!רÅ]8Nù2ÄÁ_ć¢k·÷É÷rèúgIÈ^ÍÛŽŠWdfu9xsÑ¡Ee¾@ÙYê¾ xæà,rÛÛùQUì/+ÈÃâv'„B¼’ˆÌZfAL^ÐËÇhÑÆÃÓæaà¼ÓpNƒï~¼5Yš|D(P×ñ¶Êõ¦Ô,ÌgÛ‰d%KT¶ÐÑ£N¢øQX~Ðj¾“f³™€ˆÉ UY¸}9Ý5k’ÅGk+ZØDfUÐ}À1ýÿ[1êl[‘PmÎŽ &Zuuû¹ÔzS¸„ˆŒ•ÍÖ‘ÒÛRi(Ý׆¬C%?+9Pj¥‰ ‚íÍPHB"‘õ[_b0« _=>«s#Û¶ÏÚ ¬ Z·ãÆ<áÒïãcxïäafZL*Ð@’ +¨u½¸+¬L×0rÿ8´˜Øo~È“e¯C4û³D²47 9û~ÅW£Ïgo`ŒßÅBî¶½f€j©tò8?ûøÌ&ïýfLiQ—(L&™å I¬Ì¹>ÃÊÓtu„[W -Ó_R›Åz€óúÀÝÀõ®ô“/Jºi×H\ëc’kÊDBžüî‘Éj ÊHuÈ–kã‘e%rLŽ}‘¦U7‡@ü’M½lƒhgÜ.¾P"|²÷Ý|J0¢é]¨ >œ¨ÁÞìLÛ·ä¨EÎ$JÍ8é…¸òÕU¶ÕBÝÓoê3Ú¦ù¸vÕ.q;£ú? òtØsG•¿ŸUÖ|Mìü(Š,©<ýÒJVªô—xA¸öûÖ]Lõ˜n~7ì@ˆ,ð%žcTZÍe–Ï”ݬIžT6öyÉLß´=¼Xf³ãÛµcàÿOá±À.þŒå¡< ¸®¾ErRÎPìCNñ~Ömž×þ+£“?@wÆ Þø§b& ˜d¨+úg š@äVþYrÜàkìÅRpx‰Ðþ‰¤eJ×9/KØÿ'Ãýq½’+Z›ƒ…aqû]øx<µÄ ´ºpÙ²úFœ¹1òJ¨°Üô¤ªºáœj–QBÕH·ØšûY“` 2 {þ{¤èa"žÌßD;kÕ˜D•:º€&9×.ÎÅ=T§ûò·M¡¸C8ªÃ` ¾«“°úxß§ŠÕÓC>](ü`C†&ŽÈŠcÆœ‡Ü€œ^Ùö4é2 :¶R-åg%ƹ/ÁŒn¬Ü{àflwIĘøÀëjÊDuƒmýdI÷àTô™®íòIyأǽÛtäæ`Lóí·ªóAº5s ËIm—$2ŽØ±/{Þï„Páúµì¨^"òÔ£õ '¶nãv$MÚ?á‡ZWÎmöÿ Y7ij°”`‚%¾£ã ËÚ“ç‚ÚìÐÔPáž~ç´I̧†#ù¨àûv*>Ÿ´ «¦ìdKhrÍu¿ù#-+{´èkh^F&çwòZËØ¶‰GÝáÊQv߉|ɆÂ_Þ¹ *”ës³U=ð,gŠ¡½ †¬Ñ"¢sN)™Ü‚^=Þ+Ñ{ß9œ²y›¦SÕå8³…¶bÃjŒ—7†©¸š¾.ï2Çúºõ©e†7&#ˆƒÙ#,áÄa¿pĈS ‡É˜ú y[pˆèûrplpš”¨,'þ>á®,öùl”àé‡o+r…u4¦§2- Áq¹nmð@“ógfŽW¤e¯å)ތŅsŒoò÷åºc%—ªzYÇ2]s݇{“'o€ï“æI½è!†ÓZHá7H ß𶯬ÆÜ³ÕÄžÕHÀzºùbk·£@iÁÐö[¤Ì˜Áí)󙯲mu°\_´'{g ³jö+R;ß#æëj³G£‡´µ)¬íZŽøÅ“}¤Ẻ < Éðd@×]ØD9Ìj¸‹w0ØÅæëCŒfjø€°%~ÀÒ-@¦îß%R:Ì5ÏÿdVMìqO©²I(®1i[i›)w ˜ [•N‘*Ƈ·ÙžÏ܈{Ú̯H/8r+BàÉT³®ÞÏ>¦¾9^n8­Âò 0s,àõ+í9ëØÀ6E§tµ×®Àù粜¿`r6ðð™ ü€Ý 1¼DÒè79ñg‡¿÷¿™e¿S ^Ou­ø[¤,!¾E¨õ§æØH—{=pN]™NÕßñ}ñxƒ/˜«Bö>aá™nö7óÍ”àÕ€’+_2Í4žŸî•‘Ñ9ƒsB,Ê@6pÔŒ[Oe×ù·¼ ëu^½Ù«A1¦ô9Qg`+ÛÙû Ä©©âEŸ­¥¤XÝuÏÞi(¸E£·u…£Ê–?ëL@6÷{]sz±¹B1ž`Œ.àT}4ÇòSBû—I-7‹Ýó'ÁÁÊýlÖdCÞµV)>•ŠÅR² ž]‹0šÏÀ€ËW,E{”½ƒ»:>QˆÔa°3\›Ç¦x¥Û Ö\¢¾ØNS¥Ÿ¿° ›ŠTæ "3fYù ¬ˆÃ‚2-‡kMPS4‰!s¦ÓjL™ÕßzªSÊe÷:œõ°²Sro%:}ÎGžô¿Ì¯íÑÁÅòbVa¡6rmŒ]2¶Ð¤µþíðYýÊ^~ç¾—¥ÂÚ1¾.?Z—•Ëic?– ?‹.Š$¤Jsš¾KR*1Ã#`ÕrE([?vëXB“1'ò7+)}X4žœŸÞg_IQ;TØg¼pXÁŠcD§Ï !¹Öº„¶’‰÷ÀEŸ–eð—¤TÄÈòkÉíôDpÙt³<^‰`Å…ÔyIÕÑ0ræ©vwû‡ß›5~œ!®<ÉÓk ѸFŸz\n¼{ª˜ã îÏG%á„|i±ëJ¬$<F0œZ°G@IñP6M¯Ý'Wx?ÔßWö¸æ£õR&HI;{ÚãÆ$Â?MÄeíÝïÅw¹véÍ @aˆÊíˆm„’üF`¤®Æf*õÏècC¶o:nú/ÀAc¯j0D!ŸŒÿ{Ó‘¹Œ¯i0_Ûïåâ¸õ·;¶–{FÔÜß™?Vwò!.âÇSÅYKÁ1OÌ'®[Þvï®&Ï’‡Å޽Á0zh¨ð¿Â«¹“î=ßÙx½¤Î—õ#;‰q%û ¨ÄÜÅý] àýZ¯úµŽ>®ý]á}ZË}ZÁ&Æý½‡f‰Ìq¶¸ðe·±žªº«.ö‚>$k¢8ôþÞ!ÊžY»kÕðZŽMêËIc•dÖ/,uéàq)­ì°¿ÊË8R*q?aP¹õhƒ@ÊÈû ˆ{µø®ÌxW2ä@ס’@Áû}íÉÑÛó'—‡',YQ{¢ÉtÄè%ärQœî±O¡«V„ÇÍ­]Ø*3®ŠÛ$QÜ“„Ãûˆëùá6Vx1 uCdI‚£«'§K‚ÝF8- Vád=w×òÚ³±ë :ÒÁ¶@¢É“±r)5þCÙ:PUµ½ëƒF¦¯¢qp!K?ùSŠ++}ö¼Yõ8”n…áÊjýk¸Ù‘©À{`;I`à&T¾Èã/mn ïù£F¬ââ݉,&ϳê^àu[žê*öÎJ ð#„¡)è”cP¹S¬hë’_–ð¨cÐôeù0û {F¥g€õ”8tƒ>®Õ»ºbCO5'¬’p‚•'c;Çðíñ8›reSÿk©eü‚V¨¥€5–ümïF­X©6 _­z9Û¸$»ãºp§V'‹FÐÄ«^a 6|&1Ú³rä%ÀÆ{¸Iü¸í ×ツ®“Sà̆éG ´k\QžIô+{ùïS²üæyÎáoÌ,†“žœpýmq©Üÿ.ÚK«Yj¶L.”Z.Ч¿ÂK¯;†Ž².Ã8Þ>u…©¤;1GD×ʃåUô¤æT¥îtŠ˜vÕÄë¨'|ô€rÞ¾¿À—¬Ûrq!8é ³¨hÖôÓyÒDóâÙï"ì7û+ÒäV#k”;ñs~¶þñá<+hk`/>+]¼Rk“7š…xo}Ü5Ù1™Ê|B‹àÄ_Kšð¨¯„37s4W°8'Àm0€!µ/‚À«UÃnrÍlUØsk‰;Ññw˜Új%À.:ùxºCtc" OG¾I·óÄç­1—Ììt™‰Ë8ybHƒ›»ÍWïnøÀñúY´¬ŸÛq» 7Ÿ¸D3U<ñ6’¼ß®î˜ü ‰¿‡¾z…F Úˆôʆ»9!Jˆ5L«QxrÆÚª‹$´Œ¤+n*ba8\”œÁ× ·>Ìçƒû–×¾ ˜¿¥<×ÔCûóþX ´OuÖzG‚&ôþIÛW\2Üð*ï.Ë&íJ½S¬wxϪ\³ A…:µ4_Óû‚Œ#Dnx¶‹¨™~fŽme¤|ÓHyÖZвHΤ“"õt‹kˆh<ìÓÏ ždMqM™ÈF ”O•Ú¢ÅÊÁÒÌPqƒBǧ¡…뀟¯?‹ÈD=<Å–Ô×Чèù¥øô4øá[ˆ^ƒ}ݦ‰ïìMøE­döë«¥ñ©™úöxqò¯õˆ*(åÒð@ö")¥¼]1+£÷“gûVž§Lh¾þ‹úB Ô>N“…׌h|ÏóÐXŸ+C<ð"¹¯ýð/Â\þJ}ˆkËSÑÄ’0sÕôºLavÉ©i5^ƒâßrÆh*°zzùb#Ÿ­ÍM¢ÄÈ#o† O7w`ÄP4w¶Y §ø¨áƒÐò51ÌØiJŠ Á´¶ôÏÁÞC±¤‚0,fØÎRgo!]c~Ã@ÞRú,h> :µä›yô™Ø®9Ž× l‡ø}Ž2åV}Â]}½D…ÌêuKVIÈFNöy&¾¹å#`ú·bŸ¦âdBe„F3CûÏ@€¤³ž =ùU— Ó”wå uiäOj Úé^ËÄÞ / >½Ç¡ë9KÕ/{ƒ;}×[®bö{Ü.#¶Þ–Š˜Š@äMDH»ÇÅ©.R@è=’nÇ‹n²A³ÖI£ŸNìŒ=€K÷²5}“›<í¨ù#8Æ«A w"ˆó.ù@rkȼ>KÁ^P­Ø; žß#" ƈ©åá,L˜«oïló^ì ðZ "q¥nUÖÆPòQkÇP4¤ÕÚØ7s/R ’/†Ÿ–f(gOŽ˜B:¤8Ïýßuá_rgÕáŸHc9©€Ð4¿F³ Ç?“ìq°öáŽïàz{÷×w-¸u 4)a ´ÊÖm"Òh×r&IÀÚåyÒ”ùBŸØ–÷>ã¹ÜÛ½É1Òâ†n”'–ŒÔÝ·+‘(¦É8ø½?üLN·ã9zz‰tXoÐÐNqæ*~y?¸Òº¼4Q¼ÆËf¦xt•®àÖeûÙNjõXWž!f{”t]/ác|‡Þ³´ S…Œ ˜c?ŒQÀËÍ\taùø§¶ˆÌEÈ#Éâd(Kô„¹Ã3ÚÓì¡"p<Ê0ªWh—þƒö+½”:¸õct¶C+ßÛKº¦ãŸ‹ÅE•~Z µ8bdvîèÊîžàs,[ÍÍ™ý€ï¹”@%< +  /C¬[”±f#ÜSÖy_ži©œídw€Ov’y& HH¶sDõ=­å|eÞˆÉ7jÊJ\œ¤þ‡«€#?òéûöÑë)bôy½b.üN '‘ûè ûg‰dô~€6nNf÷³MW@<*ξá-Šg^šÛëÎÅpÀoY|쳦ï=Ï»ì¿s·¤ ”×Í–ð7žsL–S™Èûoÿ}ÇÈa)ͧêJ¸Ö3|WèE †ô­4ù5v­X_¡‹‚èŠ%@ Ò•:•w@åÖú\‘VìŒÆ8÷\7Õâ4iÊ‘AMº‡t3·ÔÈì ˆ0^È6@A÷ÖŸUë‘ÑkÊßXŽˆ¼­ÃŸ0„Öò@èÜ“CbÿTÑ`Î@åQmº~6`é%àœ»6mÝ„·ËêºQÅ ¤ ÝÉSqÁÍUˆðp4,t0U÷åõoz 4üaþ-ÇW@¢†¾ Ù:·íê…´P"M’° Dâ= S+(ñ{Jm>þìîßSª–÷Ôrãå‚ú°8Áü]«ëE+˜xQSv³i«hå@zá„ÐÍ@’/>Wnd†TìDqò˜?§%’-š¹çªÓ=õßTöÜ Ç9èz{9.SÑñØÏªÊÎ?½ó9ÎŽô€Ê„ÇE+N.Œ¯ÞÙ%ÞÓ®>ª. ²öŸ$Eù'Τ®S“ôˆØ—õxTI£uŽáÄ1Z¤ªù"0oH¯Cÿ/KX^åÓw-Ö%›bÎ…*0B}¥*ÎÅñ}A˜'¶ùÑë/ÜŸ3ªMN9Â÷7$Ej°qõ°£?X¿+ëا0å=V¾”jCXOŠ2) ‰t FL{†PSÚ:+rR yÇ‚n¦D0—h÷í5äL‰2ám`ôò瑵zna…Õ>ÎÖö™FV ˜y–3쥵?IDmŽl×Uò@ùª½ŽŽ›˧Nšè,àk$úceíX]¸t•½á°šZMù!”H Íl‰c'±ïÐ3C·ÎL f5‚•±ùCîzû¥>V°˜» k·„&ûæŸ@ É/°˜‹œkçp.4å€M-ëh×Ü›Á#»V÷^ŸÔ%¸#€aª<ó¸±ÊŒÞ‰ø_]k”qšàY•'¤¡Ïü«ÕpäÓN/ø¶ôæ*zí -WÅTyõÓYsÄ#÷1ÁL|;]h ²çë°×GnÊ’áûz¾­Ÿý»ËûzèÃöô}[ûwcöõµá¿·Û°ŸÃs‡Y ìþàð—~áäɵ"’?Sß0nŽù=¯f•þÑà3¤ÅŸð;Yddy‹ÀÈq°àd_8dê:Û8V I ÙØŸ„î™w‡¤Æ'M þCVK²÷~òÏ=ÐÇ«eÄÑ€Q†µTBs0øè©ÆÃ˜˜·FOj“ø &u6ZÁöXæ`ðkÑìºé­[_”ŒÖ)\³W1j?õ8‡Œ79Ëm ©UÎåñ^¾7 ÔJ mnÆZ2Dh»ßFÏ=;࣢óÛaHøhDúX³¢ÇEàT{ƒÀÎÅBõíÌhN?ÚKþ´ý«!$@ 8Æ(÷^—ƒªÝÇ–ïG±ÇÖ•çm“…Ï(±Hµ¼ÍoO²”f 2cÞ5üzBJøZË%]\ïöò¢¬ÔvO÷L/î¯tm$Éèe®X ©¡épPBecîjæ­Òx6æ)[°m àN¯ì/ó ‹ ÜÕCζ¤^\)«¶ K7>øŸì8ª1”hádÜèÅ´ PØ@Ì„ô±®C‚g‘h®‚/ðFþ7—ÀqYL.°¯oÉõ^FH%w–'/(®mcÃö›ÌGdË ósW³=! ÆY¶×+ÉôIÄšè^1>Iù ôQÚh¬Ùú¥5·°p¨¹X0ÚOͶR}ñ|Ò¤ú"ã©¢8l¸&“|BwqÚ5 õÑ"órWãßr4u­£8 cä,ŒP[%Ô}‚â˜Ø_ȬŽw„÷X•½³uääFð8·ø¶v G ÅÞëµHvI£!X2ÿ7bÉÆóm«ºj‡(@×a .~ͪ{Jåúü•"±~žaê(D@èîëþVKu-©ñ Á¶ÔÊ”°y²(Tåêºò|†øt8§*×:~(Â_^E3”HKÎLçQÍ»´1­(EMAÀú5V ýòÄdèñX:ÎY¤š¢&¹Oa¼N” 6§É†°yÿ%ñGA%‡ÔÓ>õWŽ›9厼2ŒN'†âï·—K¼´‹0Q+C)n•F‚i+S(YXÈ•ÀB=곲Ŷý“ÛKbµz¦˜³°˜7Æ´\9ƒL ö*§ôÓÆŽ7ðˆe&xmlŽˆ÷aœ^¾ÎÂ}–¶C4K›EØfÖšêÎV#!Ž4Œkû‰²Þ¨W=#öÏYLâq~ÄHâI.sÝ`E½¿ø L½,ÇíB?#‡˜bi«ÜÄmÝYzD÷{–‰¢5ñݸ¼¬'ÊQXÀiNçp,v£ê=4é_Å„õúuÑŒg<Õ_Žyƒ@½Úu¤^FUH‡kâ¡]“—”EˆûÛQx-óö"ÓM"jª$7ž˜5Z~37/âR{3”•gN‚êÅG 3TÁÍ»/~8ÇY™pæˆÑVúpžŠë=Ža1ð|ú ü7™fˆ)äœGÙOv^u¶™2²¹`µíp–-,±âäÛùÜ›aûï²£Uö ¤@ñä@è˜ÆŠwé! Mc\³ÇëB8GÈŽ“™Ó~ú…lÞê¬A†VS½¡f5*"-ÅöUwä üQ±9§Ã×ΔÀ»¸0+mŸº=YD ›/•&•D=deƒS_ÑH¤œ”ðéÉ<·Ók²7JEWG# @è­r]á¿Ò3ËšT ¯#A˜Š’ižSyÍÝ» P¿ÕfŸk|g:=ˆÀ÷æ’ŠŒì”°V¥<"qR1¼íÞúóAEШM¤¨hÎ!F“­@Š[Î~&x»9Õ¬Çeýˆ›Àtü~…‚àÈ R©R+ížN¦Ä^ò±¡ÊÏ&7É«z–÷ CW(ÔŒ{Æ-ÙN0ó›‡V/xYÁÒœä"Á¦{ñ-qR‡*ϲá>GZPµr®ýuV”k½†ÊÌßgÈWH6ú…€_øndg@×I2(Gî ô“4sËz¯Ìàe1í¨A[¾{Ý€½/Šmõø/t\òíQ2î­—Òj6%ü¸xÁGÄP—ëšzT"8ˆ~.­ç¾åâÝè/Ý”k¨€¶vRBæÚ;Šf…Šæºº–Ê&­è‡«wé/+¦§ÁO1þÄ„ûDÒ‰&lQ^¥«súáurfʲW˜Ú4órÃæeBã­”¬ïàjÚ=ÿ4Ê3Öc$b%¼fâ?|ÔA2¾×uÁ(Á†ÝF€·ÜU®¼è ãx v’: ¹½Îb%yMc_ÞC8JÊeºâü©høbŽòHkï]ÅÅÁË!)&ˆ_ÁM·éüR²À¿N,ºF 6Î%Ó}[,_—g†ûéûɾ*/ÛèXya¨$6Ô˜›®u¸T/#ÜŠ Ú_Ùˆ„€X}#ï4—]ÑA––JGV!¢ª$ËgT¸ñ–¾ˆKþ²¯¬sà÷RrPDÞ„²5ެ4mÿ ƒ±üë=þ <ø[Ñ£þÙVFõÝ Ýt˜»»íî÷b@Sÿtb§BÌÑMû Évììq|Ò ÖØÐ ß\Í£PŠ ¿hò[M´h¢ù&eÅ-@¾ d¶è(±zäÊ•ë ÑüyªrI¬©}1-;«Dà¤`ÚY%EdZùZÁs»‰Òo(éL¸y‚о¦mó6KYÕúfŸÛ›¸åPxÓÄV'*“ógTxÛî³ÑdÄÏ mÀbh\ŠAûïú=Ø(‘]›¦Ž¹Žyóª{ÁjÇ_ úUœå6†Ð½ i]™%Wû/ë, úåÔ†cvíöVÓ}Ы’Bx- BøÏ÷U0Á¯Ñ¼êXü¥GóÞޓ–ÜÁ±Çâw¯„„Ù‰yþçT“00Ü<ªS´Æ_yJÃW¸.è…˜0˜pî'ƒù‹7Àì‰/»Ùg|2_úÙÜu„ ºŸxgÌZ0°vö¯|}`# iÈ•/Ò7-ÅÆ¿‚ßgTk½U °|I B, þWŸ йdm:šW}8ñÊÛÀH~ÔdnÿD øº4=v/Œ™ƒÞÌõ{?[ëÔ9‰<=‹ãWÙkçÖX~;\xT{*J5¸h¹k°*›q:bôY?¯Ê!7‹ÓÍ*P^$:²ú—¤“µ3Ĥ¥Óeˆ ,I¶XZ‚«º ‹ öÊOXÀ‘ݤùAÈÄld©ê}lÊÈßbøÂÊCñO@Ôœvàb:+ÆuÖ$€@lDzÎq-d†Ð˨ã¿2r`¹^•6;+‹0àZ2[ „‚6®Çì:ÇnÝÏdαŸÍ0Pa4o­Rn¹"J¼Ú™NªÓPù ‰ZæGQZMõða脃~ l®üC[qçÒ“VJ•­V× „¿)5<}ÝÑóD=Õ]¤{¡î]²@C¦jÀv+”gäkÉàØÌMß> ‰‹ŒÛeH*ä| Ý‚Ôe$=Yv/NŽ^ äL*ÿjºhä„f>„XÐkKêmÓþ¤f請\ûô×áOðL~À¡0›PJÖ‹ÚC¬±vô®j•aFÕUSÎ’lñ_eÑrþC@Ú~ÒF"¿HC³S#º-qu¨“}(ö‘×ô[ÿW»‹ýá"´FÅ‚• °¾QÔKäêÆÍÕòV%ž»õ¿SE“o{ñ…ÕÞA¢jg³V_¾@)#÷´í 5‚!{s®OþºžŠßÌRHKb·#äkž05Èù#ŽjÁzâ$üý1ªQÞݯ¼‚²8†Rm%¶¼ sz\KXåÕ¯Z2} éè:Ý '¥Éˆá.øiþòb`Ïo4C‰þý—5¼’Ä#Ll†/‘?·iVk=Pš@ö¨Ö\»±½"1MWg¹´`5¾q<ñéŠqc558-šêó‡Ö.|Ñ=6Í ‹ïtÀþö×[×çW¯®™T6>„²çÂ4ñÈðè‹×I6É=ËèÿO©ä#ŠÂã¥&‹È1KP%Õ;Gnö–­«‰L%BÉùo”¯îP–’ˆ„BÃì>Ì=l'œ7Øù>ËÎÀöè ïß_4rŒ¤¼<8P…A꣼…Éu‘þ52KtéïAì®lÄ$ÀBó¡À²üÍ«KýÊrÊiý·iUsBô‹ ké q ³@)]§ÞÛ­Øëw/“,}›0S¡¦—u½o¨—ºÝa|òª¨›»ÒpÔv  Ëx5*ÕfqØxkUöê/ŒA*tO–]fÕ,{¨ü®¥»ZµŠeÍõ*$)àÞùÖp˜ÄH$ïèëÜÇ´Ÿ{¨Ü=…܉Ê( lWè…¬[žgE.â68%®þ4·9oyj%¹•åBͯôý‰`›L†¢Åî žxh Ñ·kZ ñÚÎ8ÁgÆÀôºwGGžQÎ3Y¶Àáx/1äµ-ÈqG£!"ËNo(ÿ}­’É1¦ŽÛ/·‡å9£Ø<¿J6íaôQêWMÏ5xÀÍ’4œÅ¾9Ý+E¿L›ÒrU&ý¤ =‹„·¡‘÷D튂fžN£‹¥ °›¿›(l6RdBü™mБE|¼ýyÚ{4cfc¼œq½çeÈ·í=þ9WøH—«Ñž¾.ÉEáÚ8£\.bd´¹$d ö §¿‹•MEí;QÎÿ ’Æ<è½Kc²„c/ic‘’äwQÖÁGÉŸ»ÃÎVDt¿Ø,gÎ_†pË@]U/lI“K›½e¼¶8bRÔlƒ$ùâôd“F29ÿõùÙ¹…ô9³†U"åÌ6õSÎ䨫ßÚT¬y\D9}SšeÄ¿&¨†—w{ÿf¡È[°·˜ŒÄ1¼òhqã€~öñÅøê—¾ ?½vO©º·rÛÏÃvkÝ:Àc{:Ëè`­}ŵ‘(½¿åi\f©-ÀžÜ…½ÂαخõvÊÇI;°&Ú¢F´w"¡«\,FÐCa|/?@™…ÿnï9]QGfÇ—·Ýô ëçüíh}0r¶ªŒPþ@£%¬ÙW¬g_>S“|Õ ppá= Ȱ>t Àj/s¨"ȱGlOýûLí®^³,ĪzÒY—\:°Àf2Ü“‚#Gîû€_^ðËFROM8ì¨áqÊ Õn*nŽ6‡ÒÑŒîbqU"[BÁ}­üŠã0¥¡¾‹”îRÀ]-?cÌ€Ý<Í|Œæ–aÒÚl퉖.Pr¿V×5?rs"N‚Ó(ˆ@L×]&8Ô!Ê9óšë·Jïm‘Iè¶ô K™Øux×CêÏ,c‡vÔxúLTµª  µÌ< Vl…F·ÎëG©ú6À…!7z8Þä4Ò‡’„¾'ý:EÅڌަ³•ÛÏ@R~$&U<¤ñ#ÞÞµSø^˜qÇ ûƒ+nȵ]qVC ³ ßý`¤f vÐB‘92x›Û¼ºdפú6T.õ‹>¦Áû*Ùx+«[ ž©¿Íá#3‹~š/"kì²1ÞZuýo™p4I†¶)øXë ?B‘[#S¥‘(aý2ßH˾>6EJ•¥D±‡Ä-^î?Ù|»ÖQú$þ0uzg1‚§aÑP$‘ý"quà·½Á”~X9o„ˆ%Þ¨‘תi)}û²éMÏ¢§BˆÃÇ èïVPVžÜ¡v¬N×Q.€?!ÙBxÿM@.GŒ>ï¼DÚ%@¦E±Ùu„*¬£æFOogÍXÒ<“âç³Sm†%üíøð©b/Ë×»7!ˆ?S7™»Âgn™hz WSãNꜼY…’×U2B5ªUê±u­º‘,›<÷ñn¤æ.”¦®‡׈)¸ù†ãæÐÐÀYLy]ƒî©°ûz\BT»#\}¨cf† ;³5¿¨:ü 5.ÍO8•8ia6ç=Õç²öÎå²ÈD‹zw·Ö²Çá2ÌHC¾Þ]âh«Ì;ôåvø³—Ä5qÎQ¥(=óÃjޝïs!=¿øS;4ƶ_-ú…¬Æ^–¨ŽÚÄóœ7¥·²1¡¨äÖ¢¼Z÷c)2-ã”°Í"E<>íÍø"Èiýþ˜8® Ðý¡(O?;Œ÷Ô6€÷§Zí.rc D¥¿{IA±»Ì‡…ïXß$ÿzýqp |@ÝTgä×[5ó@œ‘ÞCÝc¯1ÿkî% à·‚ekhÒ:S„¯¶¤*\®ÖcÃØæ&ô÷¥€Y–†@t. b|PáËÎeÐ=w‘°ˆ°¥u9¨ÙÎ]§<&¬7s¼;ú±µÕœ$:[ÌM`yT*°Ã ­ke;Nk/~ÊLá ÿ ÃmjžžóÄÞ}¹Ï5Š#ÉJ52T¦ç8Ut\…ã\xŸÚ,ÎøQµµˆØõ™ó«¢²º|’D_"Ö [æÀ»V&,j_fW~÷/?4\K¾:jFp)‹ÕÀ?£Tf0ˆxIØpâ\TøËhš¡ÅåL—V AUv(ÀZ8Ó JI¤6­R²°õ¹–pfÀ 8~‰ ‘G4¡Ü[2‹Šo=%iFÎñ,ªal¯yt¹êÑ!ÆÕÕUÉü/h°~þMX¸øýç³Ö ¾Yîÿ{€|¹Ž­çÔ+­8Žöžìo¹‰7•YTV|olqÚW`üØ@qŒQý±¡}°oV͇‰öºªdý)ŒXî³>ËfvNþrt‰¬¡g¿-ÈÔÝÙ­€PÜ7Ö4-XVÀqüâ_9…d”û_NãøuÕü:‰þIóqü:«þ.Ž?‡pþ@ÿFŸÃ¢¿áÚp‡C*»F,PhôT‡“»ÐecÜÛ Å“ÀØ7ÐA/!Л}†Cáäã⽆O‘‹  þ;ÜÃ8¶bwÂf Åçd9P^ÉÁpŠôßhX•“±â§¯ºgÑ+x÷º‰Ò+b¯ 4Ý%Oþ?)ºòýõ­&¥|£öþªYÙsEP&åËdz2 8xæGhT'©Œd3NÖë’Éå[ Í«ÝÛXf¨jFüsáeß;)ûÂ<„Ëß:áí°ÓÀzøq´¢ÿ0ƒ@‰¥J*å«j.«ìW<ÚæðÆÂÅž£boÌž‚±¥9q  :$2àéot¶G·ÙaèÈÑ—‹šïr‚;Ç@»!ûq(û‰mpÚ˃vaE9[×äÇð{ÅûN^OwÃŽ¶}ó%ÎnFÑ*-¯ƒŸð)å'„6óâš×èÇôã¯=ð£x¤h£®öb+cc'pÓÀH“±I8 ‘˜ÌÔ[?äCó+{D×b\ý+߸ÀeZ“×:Fò‹ÚÒYmO‘¸£áC&zÛ4äÐJãO›€ ±›KÅŒ› K-o£k8ißl5ÒÍêÏÁ=šõ³IØ©â³TßzìMY¹ëà™¹ñ¹æ0Z¾nµàöŸÊ¢ÅµT älu{N쨘 øšÆ‡8‘ó/K–Ïz/ÀH™ž.BÚ®w§%lÕ1¨õò‹¬ÿiÎDwÓóT¡•[½Ô†Zµ5þ‘œc‡¦ºã,FH·FdÑU¥¬aô=ñºÿ+ƒC»þ‘''G€7ÎŽ”ØÕÇ\`èJÔ ZH™è$ïíu¬8ÁÚ²ZÊ~ÔWï4ê\4HŠP7›to§ 'm‘N½ }Ò¦¿ŒÍaDîEYŒÏ€!È0¦¾ø††JŠemïj¤þÕ·! úxÄ!¥˜RæJûÆcXÊ2BQò…Ú호Ê€‹†)° “Žxt:uSDðíÒŒÅDÇîëp÷Åb&êâ˜ÇjqݘžøЬ—c?dë7°Ù{7L¸pûª–Ëg]òqË#IœFÏL¹±ø#€Ä` õêª×úÚà­ñ]®ÕÃg»±‹úƒ<ÿLÍò\…ЬåoÏØ¬±¯)P4£ä"³EpPD°$žá8µé,Ê]£v|Ãå «$χN*ì aè_Tù·ÇC5Böa¥z[Š¡Öг«šÈî(_£KràdªÎÝw„)úÕiBõ„…ÿO2u}jùþ‹L¤;Iùz÷ä!¹CÌ’~7²@e¹¢g‘L¸PX1º?(K4‰¿ï_ƒ.Ò™€Åé‚õüÅ:écÀüÆ„Q5T\¦\M«Øžfm츀kQNž×u<øiÌÜÕ£]ÚðY?†44po=>gƾzt9¿îÊ%7†If°½Ûã ¾Ýç±Ñ6kPÐbr‘¶2²Pª¶Åi#Ô@ÊÿO Símï¬ÃyÖ6šö"‰<צ°ªOXuaòο-øvàYö‘£6 È^41íCŽ›k,þ†ï9ƨ(HWÞó~rGL¸V_dyÞ2æÎ䡨»$évŒF·x,½É-’1üçÝ}nû¨õfŒëdóøÃÅå,éó„º/Ç̵Dç"{à …¼ÐïŽCHKýF$¦°çRfš•0™* ש‚¹NŒ¥ Qá‘4ЊøzbúG%™²>–û{‘ìå1(F&¿–H;ØJèÞ¤újìY¡œCÛ: ¯Hr Oåg‰UôõW :µÊ.@0Áqê‚vWànǧLh¼¡—÷cnV`Ê[f)†EÇ"ØÜfàwŠ› .Ši×lÖtÙ»ps¬äF™TäP dãŒÐì–ªw[’ÔÑìo9‘ÎX¥+‹A\¡Üá×±e3;;ÐÑx7SŽ·)ÂCÑÁ^¡/xç´myšôÌ`Ýÿ~<5Òù‚½¯³žW’03³/€–Fû›´C¿1tÖ´0 zy6‡²g•,'ôƶÕÓÄÎNvO¤=gŸ~œ!oÈ­ÿµ/BqA…6Ok>*Ìk>&äßÙd•€+`cMI³è\/³‡G=÷•j¡ó¨”¡çb_‰mfZ®12R ÿ~*B¬ghÕ$H%ˆêá`J%A™žƒ•I6h]¶‚˜ ‰J§ëxeõDf?(ÀèªyŸ'–º+Ïãïï,!Ýÿ_léRóîµbʰTý¸g†-$acÃb¼l@èÌøcçH·§nç©ÙHÛJ9—d”~™>ÂP+³ÑV(½](†<÷ó W³¹Ò  Gæ!¿e.òRäld q²ü‚Ϥ(%â»1_J­Ñš¿l 5©ØùZü»œoŒ€1!nùÿ›íéïo,‚>g¢YlÄÓr4&œ%@f5¹ 8|­Ñ7§ß=Jv¹HÞT_K ø,±AbmTø9»çÇ¥¶ß s•v¨?Mˆþ7Õø‘O·^nëêŒK±’I}—3)…c…±z±‰Ïm~›­½i+Iœd½¦â|ÕNŒ¶¦UÖænßÀ'815ößKƒ9â§Ÿ×5í7QSݘ߉á< v M ©©BÓÎ'’þÏbgÔ…À$Ûª´›©v°°‚î4}å”Y¡lõK²ƒz“†hê–Í‚.rkÿÒAǃ¬Ó«©Ò:;iaÄü¤aH*(š±E‘ ŒIƽ¤NsëÓÁñ–µxþº}d¿ŽÐ71M+ÞqXRJòrz:‚©6â§Oo%èâ0/‰°'I MÅCÊ^êMŠýQ ùH\eñÇP×#¬õoJ¯Ð¬’ÏmìWo4ÿIœ3g»= `We|g"ó½©1C¨¥°î/D1Jw¬Ô¢)< ìì8¬%…Yû-~“ëJ¯â—Ùäš+šrpÌR²ß[×bì3þîŠPþÕ–(bláuÔ ™´>€ù7›Œ~'þAw»S`u¦µØ@Šoè)6€Mûk‰dMD]µBzÊ*]5ðë0`7Q¥;[Tö…±Ò°¬bd\Ën§—<Í„CC¤sDÛLÖA^oŠäïì\{)}ñFhêûè‘Û=JòÁáó]f¥©uèÑ1ÝVtù §D&øô)›ó‡‰kׄʂ›É©^jh7kxå~«ƒI¬âû3B¸Âc]š’ÿ5w?–Hmß1T­T‹-™föBºR­yzð0:hŒ¨”÷o2›õœgr`mD:kÐ>wìd½à;™M¤ž{‰Ð`Ù§4ZEðµÇó!!—ð³ ]Z¹\ÃÃ&|ÎŒr$$|Á0Ž#³Ád3Ãë¿É#‘I%àK¸=Ïë¾³ƒ8—ᨄ£ÙÁ'#B¢öoŒ¬ÿd@+Ùx\T"|PEÛu¿#Üfî 'u©C«1ƒÊR©àªËMJŠyž5еms × ™&Ì_À¤r†Ëï–ÉH×ãš¿%+¤Y8b6À ¢e ÂH‚ªX†¦3;g ŠÜ3•¾ù,ÚÆ¬˜ÙÝv; #ìgß0dí—’³¼èYŒÔ}Ç;[ Øënãy‚áq<#vì‰ùd1QÙY‹L¶• Þ”+A,ò瀡×Z¶ûU@Dûo |{5¶Fa \·€bPè(Å q¶Ôd¹ýì!cBùóÓÖw`LËÑêÌÖ’›÷$:D¦d–›n¤‰4ô[¯…dHËvtàvÖôÚñƒC¦j²0}gÛS››Š‘ßwBQ÷TüÎ P!a¯æ„Úþ+ẈyRÝÓë„ }«¾¯Ù‹ü­£2›£ºö×s´wºCÙ7«`~¸ÛKíy˜¢]Ž3„jÓp3-CK`ófR7¿ÆÐÂÅ„ÄÖMRU]»þN…Ųk­ª¯%BÈÅç``­¶hý0ÌÌ6ÑR+>ÓX‡Øí_¡m¹%%ri°q·¬¯S9ñÌ’v$Æií(•½ƒ ƃ“1- À¿xIt=¹3ø[JŸÛªñX$¿—¾@Œ05ª9¨ “ò˜ñ˾¼å'^L®Å4â!ô÷ojPÝñO|BCY_QVÑŒmàv¹«€‘¡bÙü1Š:Y¹,—ÏVî ‰ÐfñÊi°Pf\¢2* ¬S ðド—5Aªpw©WN?rîßmŸÉIíƒË/ýŽ ÑkƒmÔk1è¶bѾ ¹îÙMéM’$BÖÓ°¡¼æQýËÕ§ŒtzÎV=­LÛ‡‹/†ô.Å'!o(€²nÈ›ã2µeP_T7$Î)0Q4‡\í 3ñny&L¶–œVIÁcÑÞ“ô®Y*•UŒFÖM=žœ·ÈŽ3·A¶^®GÃö׌‚ßÅNH˜[ÜË?Q¿}Úy$ñvðê`w{5¼¸{ºÞË%«n|º‚htt1ÑBçN(rbqñ6j4´z¥‘œÙ î›<µîdÖJ+›ÓVõÕs”§¨Òókáœ*“ʸ) ušèùü¯cW¬»ÓYù 8Er~—«ñøR¢9$.^Ä*_‘QM9CXâ¹àà Cèf§[~`*µMh dÉÚÒfé@€/ãRÁÝ==iGΰ4‡r¯]GR„£fîúì¾·Lé…æŸÝqc X¢«Ó&\YÄ÷ÞJÙP¶ùÓš¥#ÜêÀÚ ƒôºÞ=µžSæ §4¤é¸J@Î+u敺öAR(&œúÂŽðüé„Õל]«Öêö†¸üFW¡á¹àW™l+/\ÿjeˆ°5…Â+¿²´‚ PÎs÷ #ÇžõFydü 5~©åDè é”Y›óRü{©›½!o%(úFËy£MDÎkçI8ê›=ý”áV\h@ ÿ'1¤º‹J úZ4ÀRO”ï-jÆ ¶<5P9Êc¶] þ‘øbx÷v*ÞzðÈÞ_–¨¹ëÓ ÄD5²µdÏ AHì‰1ðÄèÅiU*P‚Q¡ã;TÞæ*'Ð}VŒÓ)£_"èçö⟿×Ú‰‘4€Áizû¢–‰c'Z‚ïŽe¸ÃÌç«·žåÝÊÜÆìtä$_´3¸^µsḦµo¿j)<´fˆñ!·Æ(NÐu,HÉ LrÔ âô .mË(cˆÐJè‘HÁ'kDÛà.”iFlkà>ds^¹B¾äƒäE"MP×XF(ꎆ!ì¦`—Ìçt¤GSMp- è¿Ö,+…ŒÛ/ˆá —Kq§Yd嚤>Ÿìÿ i£bì14€ Øßˆ¨àþäIX ¯L^kž0±iw¶sÙ¢yÌør)"æ¹°jñ©œ±!ŠѺ DÓ.JÐB¢>þí.« Mµm’#Eò¾žÿy .òÓŸM ‡–žÒº›Ï~Âóû®\gѵbÔÆ‡H‘n$påØ5P_Uüé«"±Ú«>ö}i7lùnwb"Xõ(+H/^C7DB&@éSH¹&~vetñàˤ”1œRRìª:ÄÄ_2N_ùlœÓ箟Ä",î(¾ð,ž±º²ªÝ¬h4û†‘€™. ºæ"“®¨y¨óï9 gÐEì"Vïóiyç§± ÄÉ‘oÓžÚo«;ÉB©.´O¹¤¯ã6_8 ÇtÊ…ËH±ä·ˆ>‰«ÁnfÛ Òò^<ãü®úÄ:NZ¢; <¤Ü)þúóÖíl]^òQbò“Vrâ·—jQÚþó%ç–ò¬¡0Åyšh۬DZNUÕÈûA]ûúýrÑf~ÁayØxm¼ÂÒ3îyÊåiîA(WÇÃSú/‡øVQµ]ÇëPý¦F©'ZΣà̘L#^.•y¢µs“¯¥ƒã òƒ'BÊT“÷™ªæð¿Én-]IÔNì u}pâzæðÁôŽýÁ¢õ«Å»¨= ”‹° |˜™ÿ}oŽt£ì yÏ…¯˜lg@v ¡ò€§¥±šÉ+Æ­râ¡Èª ´?(î#Õ×qè6¨øb6÷ë’ö iGßiA×H|‡…@U_Á_À^ðDðdm7júTêÖÂORÛ|3èælåÖÅ}䃊„Pò ¯WþÈx¼QÔ.÷*ì¹Âýö)£ØW…·›úÿNú·`¬Ë&)îçê¹±Ködð¾xÐOe>>zŸ¾:G+$L™uÃï–¨ºN$h‚ VVk¬’<qü%Ë,Æ¿cÃë;4ÞxfImÌ­¸Ñ ÑœwÍ]¾+k–0w¥Ð¹>‹Õ ¾ŠÇã¦C ¨))T*8‰ÿ(‘Ï…g%ã1Ð(xlÜÉùÙG ÑAË@£Øõ7“sŒ$Í´‰p ºF„eÑ;VÚj奷&èyT……~òâ“î¦a·8¾£/£¼;Kq/s¦Yg˜é²Ú_ÎÜ6Z›ì™CØæO¤¿Q6ê…`¯ù}»à‰qˆïluKl?8EÎpùh~̪i;çü‹é²1ÄÞ缿ÚÑú2hÈŠ5Þ•KúOî0S‘¢¿Ëí)G/ˆnÙ.Î|R0Ôˆ¦ðÖýÿ+SqqQÅ¢5Z¹~ôØb¶E-ñ©^)õQ?÷9e=H,£ÿ`ªJl•…—?—7"—¾s°Bws” ¯S4 E`é[ºh×ãJÅ|LDt!'n¨2ZWA£Ce¾XìÚé²ÒZ֚Ǧ¬—]É{0®Ÿ%ùT)’ˆÅ;xQš¾<ñÛôÕ¿Áü}Â+€Lôû;Þ_5ŒçÀjo¡k×~èmqœ¯c˜n+Š©¯ PrBšÏÍ\€W'àŽ¬E¯Y÷7Ð`‚_íÍ^Ð$Úb‘H-O¸ul¢L JèÑ ½i4`±Úwõ¸çü–8nôgf{Oæ[¹z¹í±Ð‹èßAðį̈úÅ/°~~Y¯ºN[µùW‡#°«wêÖÊÍ«…HÜØP_'SN&2èÂA@ü.jþ'™îZ·ì&6¸ËUü[é\þtÎâ-[•ϰ‘g÷žF÷18#uʽ]ç[×x—ÞÞú;ÒÌ-kDØQ:°œ“é¼véÓ1+ÍeÊrCE©«Öð0NöÓíF¾ô €^òôÛ­¾cô§z“.Ý&v¤`˜Ñ$¥ºBðkO;f*í3£BK?Üåii÷Ï-®îËoõlYôPB*íx9Á¢þjy}˜VcX—[‚ÛÌ´/' {õµ™W±“í8‚¨Øè§@¹ý Fº-°â*Ï‚P²J.ýk•ö1]n¹®÷0¾ÝÆØAh]j3ÝGƒ¤úÐàȧüD-螆uÔÕ†Qg÷GÅÝÝÑ{]5 ¦§ g6ýM„ÒH'-g:®¹Š:æ-@âàUÉXp3:¶¯Á¢¾´’OnQ~-â‘€näúkO¿ <%øëéÜkHÀPÎö®dE¼Ìy˜lŒJØsO°ëÑ¢\nQÑÃ4hk'‰Þ.¢}…¡rIqÆ;x áÁýWUŽ0ìjèn5ç¾p>ÔéÏÅ Û_À£ ð¨RÏp®(€6ø/¾€^sÔ Ì TœgTÐc°[’Tt¬ðp¯ÏÄÐ\иn5¥S¼Ê€ÇýóCºþ?ȺÐaûì ™‰ªô€sÛ­¶É‹fˆEŽO¸Â·fª; A>]>äûΠ C Ñc¤z×–ºŠŠV@­ ²í¢Úš»f Š+¦ G;ÁTE+èw)G*þµB#\h0ìˆOh—‡Áj)Wú© /xR‰äB[ÎÂ?3Ïãp͸;ö·§N£øxéZn-ŸðLNœnc{º8Ps×8JÈ7Õú½¼å™[LeÄýeý<ÐþEkƨ:>´ô‹Î'kàÞíºQÁ‘hn­šA1¶ÀÆ:´žÚÒòô<Úq¥½4º:;v! M4–x{›Üý¡£¼D¤¬°%Ðs¤™êH1мi(IŸm6Rõf4Çò·ûZ]ÝkÍtìÛs¡ÖéŽ áq¤KÂÞ­QŽl löýP_RZváb2Û&¢ndœÒœŒ4›µT‡ÿccx²ˆžÆù6©¼ ÷VÌ¡2¿ßkjGàŽ”6ke¾·{oèï, L8ò[!]ü€§šë >Š×¾†žþæÜxøM9åí²luÕ“5"”’›“+iųÅúA¾‚Ÿ+Ý«Â7ª3Ä(R”ÎVŸu{‰á&15½êd¦˜H@Ú{bƒEÆKG ‘­#Bêå£Ù o¥v84‰­KjNåqjðQ €ÄÈ9|ùmÀhçÒ¢µÆlM¹üYü²ó6'ÖåÃý&ýlãŽEô®ya×"ÛÆ+LrÌý yç´>‹ ö©Ð0~R Bb6ÏŸ7µ½ê›¯Öÿ!¸±•«óü/ õ\Ÿðµ/¶œþ„|úb}T§ð¸ÿpÐxùôÿŸU U¾Ú>}iü5øj_ÂØ¾Ý!ßÂûþü|ú¾>{Ì|÷Xùõ~¼>}ÃçÓwᢿÍöü.Oç´Wá¤\_¶–¾ªÏ_U5ûY¯ª’Ÿ=&|ôËê~ﶤŸRÇ}®7Ûoÿ¬ú©á wÛc€3üþ̧þÑ—Ø™áödB¾X®«'O¾9½ ôA¨°âT ºMÞ!=q'‚CÆ=é«.”Îf:Ã÷$ío}ƒ®;èî$/˜‘zÞ–Ì÷9?Ôê¤c{øRg;ÊîÝ“±«å>AãÝ+2‚JVÊÔQöÞ‚ÕK7+€CÑ1TBv»üÃMÎ$b¶ÈŠ´ZíoË?*¼ãè¯ÿRk7Í5!úußñwJ†Ðð+èàW,/BC-5è…$hÕôñGð6H¥’âÞø1WÇÿ†òÅXÖ<ý£ê~Â9ÿ~:7s¤ldî‡ÿ'3~‹Í¬I3Ûa“ƒYöës’ ‡•e©ÅM˜P;V.vOeÂÞ¥z«ÄÝ3ûieôxSYª^ är?­²§´5È'‡V¨»Ä„ʸ©\y”E÷3°¶´Ó×ÉTOvöw,RšZ+Ã?³»ŸDzBÍ„›÷ Æ£ßÒrµPÂ+´¿t‹·{ êi2µÎ8ˆÏž‚€ð¯†B.lÆJå–¿þH}ük÷ðK‚˜X_2Âó~èé=-p;î<{ `?û«ÿVÁm˜²ÐztLÁÞ`bd$‚âÊÛ?mYb }èøý‰ù›üÜ&¾Åüû¦É7Çç$“¹ø¨a³ t0Êô%'¤©& !"÷“W©yt .:4}®—ãêéVEŠô@¼¦¯’CB1pÊû.¹­¤BÙ°–^Ð%4ݶz ÙÖ¯/÷ìüY|…OUPàBÊõÑÁ…¶e\}­sŠJ[ö€`S—àyÌ]"pYš¦—é'µþì4BøBÞrBb¾ÒHjù½þ´»º¼xƒþirXÈÅD1}µ·û‡h$áú„¨Ì8Œ©¿ Í÷²H/²ùtB€ðŽÓ¢á!«ÏA†Ânê$ˆ†‚Msä¨`ƒ©)Ä¥Òu Rì$έØIä7®Þó)£Ðé~]F/Ž>²~„«Óá x?]3O±ÿaAöS¶dŽ8h9eþª,>ž½Üÿø† ÍÒ—ÑEê™!|4šúÖÏ U0‰^‘pˆ",»0f ¢®|ÁÁ<ýŒ.Ø£\~¤ò¦€_$fŒ #uöTÎ9„„•NAPñ§g¯x;¿ÉjÝôíΕTøÑn_èy²§­Ó­”‹R™FkººŸYζ®InÈQ5A(Ïÿï(<Õæßɇ5M‚‚‰´ímì…ˆ“VîÊ»~êf*ÈœÌ,fÀ(Ìn‘ÔRí~Î/öÀ²É(@´ÎTNٛǞS)){dÀa¡ØjU³³ŽNË'ç/ó5•Jd­¢ÆSF¶˜¦ÄU¥ì§WlL ‘û×À•žƒ&ç@1|#2ÅIÎñ–X*ÙÀ~Ä…U7%oD¥ Ò°e—éõ!{ R`! Î\ ͖Бÿ‡'ÝàÈO¬@`F×oÑh¼l,¹·‰s~³·ÿ,×´Kìá…Ä‹%Y{m°{…oæ¸^‘°;¸«£‘•e Æ Æôó}ìèV¯îØÞ,Zñ„.†ƒÓÓwÜ5f÷ÉÆXw¸jÕóF\àEŸ æˆ] Ù„kµsÎ&ûy?²¾cÌQš´¾ðÉÛkƒ )O‚¨½>¾¢úöâÅ1Kóí‡Är l·¥Þà¦Þ4Ño´¶Ô§œ€ËÚ”,ô9ïaŠ?¦ÊÊöçæÁl[Y£ÓÜâÓEËlSp¾ÀŽ•ïÆ+„\̈•\ÙF BV¼ß!÷ñõ÷[/^öC†…]ÂsmËÕIã ö$2&_§ïf+(â[ÀòlE Öd6V÷Ìå ·®Ó’?ë;Ïÿ!SwÞÝjy¬þp<0KÝË‹Þ Þ™ºôJqlxC)XƒÍ ŠøFjÆŽ-(à¾-ø'd\k8’BÉ– n5¢”«ˆm5u;x&8 ¬‹“é·çDJòæàk­æ;Qtd±GáËN_ùëz؈)q¦Â›ÊK·PO”MËJhíHªIÞëÉ¥:óþZ¦„í} ºzǸ=¡bÔá¨Ìa „Àß,ÈóG¹2êßoû£f9vUÄ5©î…>‹€/§Õ°&FHWçQåQÍݦdg}·—Ùۯ˙+¸)›c̶®×³1³ £¶Þìq„â“LÇ%ú•a´dD»óq¨tðp(Puc%W×ãA Êv­.êsr v.Ž‚øhYv›g¸Ýù¼r¨8d½=Q¤œâÑêåÕˬÑ¢~Ú¤¯C~ó¢“üµ×¨’¸"ÃêÉ4î~åö°}&3¤qÑ ÈðV.¿¿ú¸öí‰i…µ“uá¥4 /GÎܲ>tq…¨O; W~_ øØ„©ro’§_ówö’Ÿÿdéz×Lë€G¦ “3ñ;dø”¼¤æ˜bâœÁ:‰^§†Ãx*ý×Ú¯ À³P!Ù§>ùÛC\T¬÷Ô¨Â;ܮֵ¤2oê‘n³#c{üÜý,#=¡uqÞ²W¥–Q n`ð?ó‚<c Þ»aƒLSjéÿxQPwWÍß!rë…«'ï¦3+Q®Ú—éJcZÎ\´'¨“æ/˜;VÞe2¹Å¦íÈC’($vÅ;•bî’.c^š!;Ýë^…Á¶ÐÇ}™Ôq6yŒ¯´Ú±PÁJVÿëûO·©1ס;·qŸôÉhúøoS„L6’K›ÚÜ/æL_npÐ¨ÞæÒú)¼ö -Œ(“+Ô69›F€ªSÎøÑñnHŽ2±c(Ûoß}ïU» áU§3ú/%äÀèïB¡ÄRŠ1¦$Ýmª:Y²«c¡ÔwZR«œðR …£Ö¡µžiÒ¢Ìvw¤äz¶”«pöüÀÄ2Òá.C£ýÈe~Õ#ûó‡É[›£ªÙ,{ D™æ¨F.B4nSK¾1ÃhW»&ƒd©JoÛ¨½“š¨A#oà*À_ÜŠ1¤ùÅj*6U\Ú5ëŸAÒ¼xÛ’X’W|~fDhdbùïþ4¢`€xëêp­‚MçBz¥>äB¶V­H„ïÆ_¨ãÉ·¬Ú„„y{ûµ'EÛ:m©–6j ±¨òÇðzS¡ê+`¡Ìq•>@\ó$®{ü·&…Ä-ðAÌ—$^ºMt³wõ‚17ØÎ¦›¿+¶è |::êjAæS³²RZaH心e&%ÿ{ýo`¶Æ™ ø? ]:Lň¬8ÉW…Ï£GQeà`Á æšÏõ°PÜY@+$ïå%f‚r#׆^úúHå·$à*zïÊ“²A#ž ´€w£vØèhèåœ×êÒÃ+O8.cþ[Åk¡D‚ÞJ‡ä«°Å4âó;ÓAÌаMŠÉb;ýmºqŸÀtfP'ÔÉ3Òãyjí6*6üv[œEYò­(8Q xßLÍ|×T—&±óT̤IPˆ„É7r Å1©nîZÕçŒj7‹P÷NÇNš–MI]œ^‹6ϲ—ŠëY07^†±³ÔP¼•Þ @¿MH¨¦’a Vvd’ɰ‰j´{"Ïñ@‹‚2~¢÷où%ÈdÇÇ 냊êÁ30ƒ>·Sù–èÝ8ŸJ¢!á¾]ö‹}lBôY¾"$7.ëä¥:?ɸ£/@`« B†Û&1:€«T°7·¨ Íý¹10ÕÚg{µ™ m1Çå ÇK*£ Öjóȸ°\œ(W6cÛŽVW‘ f (6';Ëšà #Ê;Jútd¯ÊE6÷*·ÃÊÛ[€Ô2 %wEY¶[mÏÙPYT _ÿù öç~þ(¾é5ó;ÖØOW#B}‹ìõA6qçÃ<çÖKHà {ÐÞ›^爐žî¬"5ŠÔ“ž‚î¦"¨ãûžØ6Yl#gË‚A·Ã”Æ9óÎè8µ¾§úPM3Ș¿‘ ¿À.о± Ù›âõ¬«ÞAX']¥#ö§%–OEäyΆ(^àfõ¼/ûWÿiÁŸ‘¨^aöÁyê ‘³hvþ¾€Ë0×lŽ+g­ëô£©\~4¥*œc~[Ì«—c ÑJò,m2ÿ=¼k+™¶ …ÞY{7Ô hì8;¥ÄZòõß Ñ¸cL.Ô°Ií@5i„"ÖѲ©œ*=eTƒR ³sÛÂÞØÝ(®·=J¬wܹ'z\PˆÅÍAµyû©²Ò#AÎü(³ÏýßÒØF f‹—˜¬Uç˜s«b/R?Š_$c6ÖÖzéôgp9Ù{ý¬:<ýtŸWLâ˜Î‡Ø+Övçb”AsãF€æîhñ%òÜ6Ÿ~Áý“k Û†F:.«ƒÝaD~ÿtTkÖaøgc¿/±!ƒVDàù‹VÕÞuø3)„^½§P~5°žXš„WŽâ^æ3Í­¢vÃqÃÕ.Õg’P<'y¬žŠ.˜?luöÑ … ~¼•2~˜+Ad‚ØbMèÌ3¢,±Q©s>剳ÛJg÷;þß—žú/;Í¿¬êGH&™5V÷ïÏ‚¡aDûÿÿkGe÷ºa÷‚`úŃÎ[Åg²(e€ã¡oë^¶‹Ah©§ûœ $… ä8êK_/Ýcæ!¸â‹Fq’yÕEDF‰6î”ýø½»Rj*k¢ÿg¤3{«Gø¾„îÑõ”*;ÌšRdOèXµxµ¦²•,HCZp™ÀºfÛÜ4èɳpd„‰¥FËÞom¾ Ó9~Ó„º˜×÷úù¨´¸JF¦ÑƒA{e¾ŒÖ®kITXn(ÖOgì\àÐ*€áíCæ0cf;XýV!yúø‘”ÿ9ç¨&³‰ƒVhdj@DŸohš€[—ŠÊZséÅö–šöÞ[7„qö™fÞ(ö¶@ƒ|@¢NÒ&paôKБ_Sôô"PÃ0#µ©¨"M)àK™ ±'¿ý-8kõ±aÖä=QõFvŽ…¨bé»M¬VA˜ŸH0dñ¢ð¢Å‚¿¦ö‡0'ÑsãÃн`¢ÁXƱÞ3›i×5£ÒJ– ¨TôŒìSúrN#Â)1½¤B¸Po-=©„«þŒ5›Àwï´ŽßNȺ`ª´ç£‘B¯—SÖ÷#à:>(' ÀqJ^ÔgzØnuzÄâVš¥ìVˆ8 ;×…KàÊËîÅJÛ_в}Ÿ5KŽ1qöj¹Kpâ ×ÈõdœóOÓ[*2ËÛÄz–©¥¦¦Ñ¢hqFÄV[`ôòjïfÏ>ÿwÛÆàeÁ”#»Œ’£Ï€wÅ0¤xÂ}èQí¡ï”/peîã$¨óÀ`9¥=LüNŽ-vËùêXx”±ìP :ôª¡â#;X£ªÉ£þlö©'¬_û+ºÞ[³§ø9õT2ödø|u§Ú:ûæ{Ú#έÐ~?šzf¢Ñºž[¹ Z% ¿á¤³²µ}AN¹ßÑÀ‰»¿ÿPu¥Á?"eËù}rI¶ä“s†›mÀ¤zÓQ²¢B×¹éÝ%DVÛm¶àHm·‘êç]b™Oì‚€þpA„©ït= $68q,w•_®õ?ÍÞ%»Ùöo@rÆIÊ@^eä±÷$Ë~Ò‘ S›—™¿<’@i¦‘©ÏªnÃÿp ¨+ÿ~῵“Å¢ÿIÀu´­©‘I·ÜøÎõR”¦RŒ+y³”¬ª¤õÜgÿL&>/"â„æÛy ¯íœøP~Çedƒƒýðý›DŒËU¤‚B_çê‚äܧ×™œrDrÌ¥ ¸dºý*®QpçóÙP±ø¶ŠÅþí;ó¢@÷M;d=uÀÿÿˆ“‡ÿEP˜àÎø»Ô»€WRÌs ЦéÖˆ•ŽT=áøå$Û†½]ů8 ÃqÐÒ¶Œ-Mœ_úÙ|ç(WJ–¹äLîHkkk­$Ù­cüØÎ÷Œ˜@T ï¤è*I5ö¿ÿÿxFA<¯º¦`ÿÿ‡H™Ã¾ô» ì‚ ¹ÉÝù0@ðû´ßxµ€©Î+Ñ•l† š†Ý¸W+­mr6ÂÇ¿dÝ j!” üg61öÍ4‡ß~pÕµwÞ5ñi¬}V!K-7$Ð #ņ7£Â•݈¾þì'%\JÍA&íÀ-p5Ô-ËÃ=-æ ¼ûÄ5>.vâØMÿÿ~¬?E5¶5ö‚ÿ^Ôÿ%Åt†õ„$Ú9¦©‹Ú´ZÔ~=üÜXÓ…²sÑ+攢R ÁØZ"É ëH+†xõt™ì=)M•øf=¨ˆÖÂÐÓª–²ÎÄž°Íàk•wdXS\Ÿl€£ï<{ؼ¬_JŽÃ2M²}^ô1ì0kÀåZ_æCö;²uë:Ã~O¾=lîuƃ¥¬XMWIr`zQ‡u³Ys¬œ0]T”[¨Cí΢ž=ƒ~siäHÛñ —Xãºâ»ãD·ö²3Ùrä’û êUÑA ñ³ãŠ;…è«ùÂG…ÚáÀ¿õ™t½•Ý;¼§&«ßßÿÿçW£™æ;ÿ|5‡!ê"›mÃÈ$Ûm¹">Rªé”ÂI$èNc$’I.A~"CKf‡ŒÔ¬p /øÜjkБò •¹ŠUj­Pç^»ÅèŸ.Ê2ZŽ—ï•]/©Â³KÌ÷×hÊèÙÃã¥[ü½;G€Û/ è1¹âØ/˜VZ¬ÏV¿$Ûü|Uf~»G%¤sÙÐ[`rë­Ájÿ£éàl`/£³£4æ#wÚ^â·|PãŒûöFg;˜‹ !_z»8dºìÓ7b¯;ƒî$Ñ[ñ²ós¤.¯ÿÿüº8læ9™ßªRϹ‰ýçÿs¡—žÚìpŽŽÿ=ÝΛuÆ0”§gý|µúyÅÃk(nŽÿ=Û¬sÅ¡¹€Œ¥”u¸Üñäú¤TçýÔ>t„¢zÌcE~“ÊpÅ8†%P¥ïX7à‘<.:ë{ZW¥ŽSŸ”Í#™²Þ×Èþü®M<#ç¼Ñ'×ß]$øËÒê´ÇÝ&CMà ¿µ Þé9:ÐÍà–@[íûú’Ùû—‡} ƒ€lSÐì0Ôuôd-¨ÙÇG¬ŒF'Ïåˆ;Îkß «˜˜½€Ž˜š¿‘˜½Ü9“€=’w9<,Éß=xWøµñ°+Ë-oÉ_a]IaÕâ/dâ¯-·Å>ç÷J(ò*üÖ»ýŽ¢Á1D7·Ñ¡ñ~ª³õ[:ùõ·ê #áÝຸ.›õUX~òí—>zEõhGá½ü;½õZÿ[ ›ÇáÝî½ß;ŸÕ †ëðôóî¿Õ|ð¿Uú«úªÔáÍ¥§ú¨¼?Ýø\慨ú­?Ãoøw+ê°mB¦45Ö5AÖÙÒîÜÒå€ðÂW5$à £ÖY´MâåïUØ$î­»^&ŸSã ;‰ÖÁ&CíÄ–™bkáû±ãØj½ï,z©Ê_©ÒbVlõñÖ=¡Ö[zà‰&gv“+q(ÒѲl®Á#HŒ÷ÎûWÓg¼“d;§g|úÉþáÆ3Ôegë:¤ô(!—kãÝO &ß-·†;KlÊyc©mV®QÀ-m¿`Þ!=–9÷{ÂF¤”í!Cø9¬ô«ÏYtåqâítØ~Y©ª;¼Œ,=«Z’ užn0µ€5×F$Otô½r¸® Çf÷p½á¦¬³¦ôàLèÃDæ-ü&é^Oÿ}Á©«ÅÖ1h âh¿\>âüzú…Ô…J8øJt?|àvHn¢·´Z%·ôŠs³ªî¶ân;í€\öT]@Û6¶+}ZøëcfnoeÇ„^ú·Z6Ï<µ&8µ-\I<¾.XsîÙÞ‰ïÿáö.ÄÝÎ,Eî… ùS(Šº¶ú©)ó{±÷nfgW}Òÿ!ßùÕ¦a—ä¿Ê6ˆ~h¬%VeQ›¡6ƒçM]RY“FˆäÿzXÊÍ™zvþ=b ¹3ÿZ*u%Œô­4÷wwwtÊîîîî™7r¨ÑŽõ;'‚GKd£ïV–×ÿø1=J—¨NÙŸ$¹µ‡;M™ÈoXoé–xPë"9 ‚œBÓo-Îì_/ᤠ?zÇFªGT^Þ7°ž°“ý(™lõO‰ÆªSÄïÝ÷)ë«ÄeðN!óoÔ™}UU|Úb6Ûm¶„4žÒwi­‚#—6j‚ݪøeˆîtÞQo2?ÜDY2C¨v¦ñ‡ó9×  zˆ‡¥œ+¿ZžÊu¯MâÉrÓ]by>­| Öúk6È¡P[©_î–ur;ɦmH‘%Ÿvã™ù‚LÃTs^ä IþDÌLµÍæpá٠˰²žÛ–´J³µwvˆÀFrWåŽ_ƒ§»EÇPä×Ðîõ¬5¼Ç™q´9pKÓ^ìÑ;Âám®HN£œe­•Ú@ÃILÿM,óÎzë²z ›iä "”}5Àv®’S¢ïïú¯¦Qå‰ÿ-õÚ æÙiV0E¡ sx#9” o¼µnÇÎݵ†Wi°ŽÚ±Uœ«jÿo(Ãÿ~’;y­C´_ÑXÔt C*&ç“ružô¢ë+¾4ÎcÕ,‹=xžPºXÓ™³ˆ$¥¨­XÃfìGHÛí>ÙˆÞQÿwG»6•±§stÔ@æœ?ÁÆ+°^,œÂB0ÑC0kJÎE6rÉ®ô¯´ÌL †fUmµdjØ>ÉmI‹GÞ¹ƒ†œ9òá`Œà¥ªñY¾Ê÷û™¹Ž”g Ç„Ÿ’úÃ{ª;GœD`AlMoÅÐî¢Z@%I¢Î*ÈwÂߤàQeÛàÌ Ëžï‚B‹­ $ç^q Ó]øPD+ÕޤÑ?¡vR‘œUøÛÓëW RÌuŸ–ˆØn¶ëyMÐÅ7J°mšá8öøÿqfÃúØ>jöf1gNœ…¿¦ _TÂȹ-Å`!‡¢[ȼY”!‘ îÉ› 5s(|¿ã7Ö™¼{Ä&$†0>w¹†o"æg³ïä*»Ð(žGñýÛúø_õ:WçF tz^OÁª#¥•XÖ÷Ãy›’øH°Å‚׬K/·ãtkÁÐÁB‘šx:—Éæl‹¬ˆ¬K±Ê®=¥v„¢šà$RÛüÖQM…RÿQ4 €º)rmlp¤ø¨J¢©Wq»\H/šqtN®í"ç“ôä{Ýo¨ÐI0´·i¦.·c!Rãã8äxT«*Šý¸¸:º¯jáIÏîFŸS¦Ìg?æÕ#øƒîºLa_Õ›°à*yYæZëªøíO%Åš·Ï˜¤ nÁѨ‹X)n‚Y/NÞ¡Ö†¦Ê¯R¢0«·»ðcµî¨é¶?/óæÌäêÐ+|_©ºw½V»äïI¦C£s7°–Lþª XLpŸ-´ì÷nÇÙì=užö¹ÆO¿¦7È|R䤨Zk˜}£”Û$¦7ÓO6œŽŽ×:ÐéÀ.Ú>@Æ•Ëf:>k^µ•Ä™Ðv:ñK¨z»ALï½o,Í>5ûñ?çAøR^é[ ªó×VJQÓª![EV“Ÿ¥Èî›Îħ1ë©f¿f¬èš.ôÈ¢ Îè;×šå¹ °ŒÕÈéŠÓfêKîÈ¢»ŒsdÈõd[ÿl¾1ÌÙEèTyÿ :Ž šÔùô(¹R¥ ¨Ö"é°„ƒ’<É»¢“Ñ”Êâ³6s Ñ ãrzFÀÇ7ã7ªíø Ct1RHÔ•©%:×w©’RÆæSÅÑ/ÄK­ðòBÆ(þüþGýêˆÇ}(~ôS9ÿgùw‚D’w ~‚C¬=j(b¿þq#±¬:Ä…Ó•®Lwðïû‡ÔÌiRAsÊ-^6]š¤`fªEùjLKª:ÎõÑH{Íc‡/æÿxPy=o¬còÛ0Ì‹%r\šqkæQêC º±8©­¤¤]˜O ¾nã=Lî"¿ÓájV?½!•ŨdÈVŶÎÙ²UŠí&õ¹‡U)ûðm~o¹/S„1ÜÙWñzg ?>×F᮫ 9ņôK÷EÌiræœTÒ múkA¼úš?W.;ÂA9;Hw«~¹BëÐGÁeWò@¶"Å2:€³Oµ/VÃÍæ#eE¶[zˆiŠmVTmÞ–¾ËάVû÷³/ý²‚Å"SÙ¸¤Nébš;åÝD¼»®ýQD_òB Dö,|~>o‘[NþáóŒ .Å]áÕQ-•~¢M'VÆüÝ ûÇxLçÛãŽIÍÒFÏ¡SIŠÓ”™_©ži¸ë"—±µ‚).þjQI€)m ÑH³áÝ t"é‘Ý ,ƒ$mŠcNæ’K4Új@\ÿMºEÉŒ/ò´ûQÙ+ZÝèŽ6öÐR² —ŸhÜ|z«¬Á{ -¶ŸôWóÖî¼H€ºÍ•ù«ãä% ØMÖÃ4â¡*½‚ª¼V/ãš,p Ëavó’ü³Qh ²ÄŒØ W¹ÂáùÁ•phEªR:÷+®ïŸ|ù“/HÖü½Ð× &¯ =-ûõ]¶ÃX€x#oŸ oHCZ?·æ¯óƒ ¬¾Ëðuø´•„ä+V›AýèV`U¸aûQ †³˜Ãá8ô@‹lyí\i@FÑøGÝåø¡{÷P:d“û§b0‡¾/‚¢"máŒ?Z¢[þn(Ð:KZ‘‘þ€»¶WÛK„–i¨ÁëŽA1z¡æîtcŽäë“I¼õ˜™Ú·Cv2­:¸ØØý ô? ÒÑÜ—> LH·I$–8ý¢øPkPÁjò“?´à?çLùßgØ#íã¤ÔSâøÿqfÃúØ>jöf1"\5çQm‚öŸ‡ªXè\—ØñF†=6°£Ê¸]œ=t:™Y6./)ƒ+ó¹4óÕ³ùñü,d£ìS|ø7i`8ª±Ž"½MÜ&Lv›+‘ñ"ì¡{ÎùIÖ‹ûÝÀ´íc&†ZàÀåôÍË®é óüW¸oÇŸU\Ò:½Úá~²ªS)ïîv‰Œ<¾NÄ´±%ÇÔ+Ãë~r¢Jp¡l.Ðøª§MkÕt¥ÊíÐ~7tÒºˆ0K$ë×î¾K±Rò+Š“ÓîâÛ“¶-Xâ´tA»+;ɒh°Ñ ¯-9;øXáoÚºoHM ‚FÓðÏÿ r¦ÜhG†³×‡5÷Ý}¢ûPsÊ,œ©Q~QZ=ò·¼ü²ö‡øM‚FW+ˆ-^¢«Ó…WP¦áéMí ¼x¦€&¥ôä޷ȺœKãÌZèTïŠ)¶Ïß ¸æ8§5±€ –ðÈ¥ßýÜøñÇïÐ:^ Ï-Ò1‘ËÜaøšuœq$f¬€â^Vß$fLTšñÔo*SA£>ŠV{íÔ‰Øht㌔ÃoRg.°ÿ|dÇu%Ãß°Ýgi6TCŽŽ ‡¾ž˜ñ¾Ï½ð?ôŽ­ÈLèá/pÀ,ÔôŒÆRø’Dû·9Ð#ˆ¯õˆÁ<jzõ± ÕËœ÷A’¶;UÏ(Ù4Ux:ÉF¸’÷5^5ñ;$푼¢…ÖèHè-þ Ð;çªÈëM»µÅÁÆ!´Ç§Î<.#aü|CÅã”GÒo÷“Ú!1J§r›¼IyTh™¾âG©í‚ÅÏ7©^h—ÑÅ ÚE%¿þØ(ŽAȪÖáa™fkÎ -ŠXÿs5¥_a\¾çLÔ¡<(9Á˜‹ÞΈèD„é;cš¦›h¡ym¸4 !“sa²ÿFý*^#)¨ÍË`W7ñÏÛû½Cý⟠$’þÔÊ則Wv3ÔßPr|ãØá´äܾÚm¨.'ˆ?»Â6X ê—~cøÉàhïÏÑGå”Vòžê0चþø{©X±GÔòÒ’]oDâÚAkPòư9­ Dñ§CE±¶vpIÞ»#­Š÷÷ÕþEs'’ße•7é1)x<(ÇšÐiþ}ã%–pd šýï9äœӆN¡–¹ú`Zw†F–leoÁMÿm¼t $Êž &ïÉ©ºò£™ó†¼çdÏ*Iå¶"U‡åU¤¥ÿ@‡¼pÏË-(ÿȾ'¹´/^y 5…_ùÃ$³®Z. j‹´PIˆXx[ ¨惨’+Æ‚Ž\tc Vo6¦^g)Ö:7îeÍ|Ž⤔SýÉ Øcâ  úð~%ÌF€¥ÿY‘ Af!šVüz´%$Ü|}ÓN¼‰Ì;L§Rƒq]<ޝä)Ó‰´w8ñóxÒ¸fƒ Sˆ(}®Œ“Z ¸*«Åy gC2è‰-|S&ÿoÚ‰õg÷ÿýátË6ŽgÆø3H£%Ý­Î_»¹i Y¦±ö3Fæo Ô}“»A´UQ+ász¢7¸— ÿ!ªˆWΦö™ó¹»Í÷(sP•€ù4´ `öyé^iÔJyí_¤Œ_óü6ÛV;I1l/@¦Ázˆ¼Jiô[G±ZtMÁãBìàfªvd òáׄÉrñCª€J†êx¿ñô8ÿìÂv»F÷Ë)ôøP~Èd„¾NäÛÔ×ÃÌ“ˆöÌ„ÆÖÅ÷È LøÕ»€V±4ÕŸk‡HÅ“5€ #cïD«óÒ°œËøÛhÌk]÷?ÏsÄñ{ÔŸ`¤ïý”VøÜxDλúõ´‡=äq`e%‡‰ÑLÐ5+”r°S¹h;¿g(D9Ò‡³¢êJ_ž—2üRÞ€-ð2OVÈ‰à‘’ç(ç_æð[›¼CrH4õuÓN_É ½µæÍ)IáA½ÚU¨üÂÚatï1ö€)ÚÕ Nˆ9»' ÿÿ\9\•ùË£¯˜“Ý ”ügYo„( À`ñÔZ³Y4Ë€¸Kˆ !tëA‹¡ÐHvþ–/L±k„»ZŸÙ€øÚL±…ˆýWþ½·ECazõÛ½Ëz›EÝðeîVÈæ“”¸ ÍçLö~r¡í€óa¢ïªÎ¾¤~|ö êПÚ×ûnOÛ°?·B¸mG}UÆýT÷êdý´ÄÒ!ÝõP_í©?jwûXÿ6àý«¶¦ýºûkPöU°Ör «R±`ËàçŹ\„dÂ:¸‰49U5ërØ«[Õ Ö×ç¢lž³¤«w_PÛ9ñûe ‘J`ˆGÄúeK<Â?¼qp¦»¿PèºqeŒtWcT]X‡LP]„#vÆï-\_ÖÏY­lö= ×Õ¶“<Á€>úãTµ|‰è þÔùú'P¬ˆoµ“Ìzu@CÞ”á¯Mÿn¡ 碬ïÚä½Ã¸­úÔ4H+BÑ|t%š ¿æb‡Q%Fšï‹ˆÞã@Œ0㥔 ûë·†ÜÜ$«ƒRŸ…¯ÀO÷ø/ó! <ì(œ˜ý|Å:•Ž_¹ü‰òµÇ£æt¹ô4©»5O–ÿKGîjËtNZ¯Ð¢_BFI`å‡f©Ú:ü¸ŠŠ–ÄyëÌ–@ÔQyJÍàâΧ×iR{!šÿZßgÑÖè¼¼ˆ‚Nñ–­-–Xº’f"7'ã> e xÌѳo·w\ävÎ'í¥'¿ÿsþšœ©é†•+U•ÎaWjã8L¥Ñ{ŒÖ2n¿ËÛtÛKqï÷ ˆLvÃl_¸ s³ªî¶ân;í€\öT]@~3Ýû)mi—½x›×ãP8T®Åó•ZðqnÐlwߌ’йã"V%¿±{Äõcb@º%YœNòx__§…Oì:âQóñzã»þ*’}6ú­á 2¹÷èªÚ ”XÞ3ýÇ„.6ígv¢Cüå8sî6ÜÈÙÉ.·Üÿe*úZ‘ ãxrlƒ^ 0¨º#™ŠÎýtÈ NîÇîõ¸e€üÁî\õå'‚GKd¢ïŽÎÑ{ŠÚžDÙÔ‘qq<"ç7ˆK ÖýwøÉA‰k›í.{å%ªÖxŽáNÅ`’Äq¸gr[r|‰êÀA÷ð"¸óH˜²ïæ{È9 ­hÔÏÖô" Ìè¨ïïxþ>g½ ”{¤÷„ù´äËLãŒÖ: Ì‚#ž“õkÛø0KȧPÐ8‘ oøMû‹Ìµï²ú‹!°®ÒÌ* qq·!ò 9S Æ¢î1:-'^#*4jw8Í%d¬]ð<<Ô&Rë•,NÝ»G¯Nº=$W/+ürÉ ¥yˆk”ü\?„¦L>6Û£†ç|¥}*­ 0Ý@äÀ5=Kùýer| ÃßôyÛÏ¡U[Wޝ^4&¹¥õ<\@à÷÷7®ó¸ïª1^ú…›é·”<³¨—Ñù$)W,+’?€5Û"Ù3àÞÙÒòÔåj¿Ùåó¢¼%Ÿ£r´J³æïŠØÆ+TòSÁåÚËB­Y'gï0= &mµºïwî´ƒ'!ç–Ât·l»7e¦Ð}Qóo¼ýXV2Î-u×Þ× ¨*Pô38@O/IJ%<Ž‚šË³óR ¦…á‹ ¾x»A@þ¸)Žæ(a'5(åc§#‹vªg¯àÄGg?dLÍKÃ3ô0C½ ˜¢ §”"Ã*oIf¤(ôOï:,÷”ƵKͳ«äRèJiˆÿ2ïV@[ãÉjD±BŸßUPÙgDÏ×ìtªþþ@‰£æñ{T0f3Ç!Ooõæù\ÛÌ%»þÐeÄŸ6©ÍákwõIs÷§ír’L ä;dqãçÿ*㦞;.y¤ ‡5s‡èÕ—˜_ ‘Q¤Ý¼gg_«f5a ½4·X‹Md¦%t 7õC)ˆ¢uŽœ0ÑC0kJ—’¿ÔŠÒöYÈ@5Á”þŽzKw½<Ü]7%Œ FäÚþÀGs<Á»c9/ØääC<5†°OF›©}1è‘Ë»Æô³‘Ú±&à>’Õçwý:CÐ6­Þ %ÜùŠ )FTþ¿öáäI͵PÀV‰,õœþÄ^^:>~{Xb+kfæ3öäº5»µLUâh_ª‘bÁý´åïÛa cËFæyoò¶.Zc亵¬Á…éY/.´&rCz)˜-vGÒÁ„_$å·üÛ0¸¹ƒ»¦ðNŒ‰’ïîs¥±«`c2ÝøPD0wrët¢‹¨Jï³¹‡*øíåß2㛈ufe kõ]¯Û€.ÖªšÃ;¶vH’­ V‚7:ÕÏ»@õ„‘çZŠÚ^…㈭q„ó„`D,cw–Ëø³ö¹ey<„Ó"\•4×<”ÀíßÚƒG=G tÒêì7åü ûÂì•?ðÚzkè{€Pʈ <õ¦—6Z†0x6x^ëY)!ûpì:,žJ˜Ø§ ¿Ø!{·/7—y^ÎÂÏ©{e8…* + k·ØÚ¬½Àè?Ü?zrFIÆØƒ¸#¶œÖKK‚›²M[;au=„Gë©êûo-FpöÅ3¥Îï‡ì«ƒw¸_¿þj±$ƒ1fÔ»ëæX´/Í£õ˜ß%ï_5*âéÿ‡™`@ 5/gŒ"g%ƒÙ“†_B‚B°_7m¸cߣ4Yb««ƒ¹ÎéRvú(î#ÅÌ -3å-GöS);ìJ,=4$ÌÝc¿•óŽŒSLAJ ÏP!Œs#/*¤ã(­<­¾™½òæ6«ÓMmðbQšµ£>suwåÃmŸê&Óù®h“?kçYå˽çÒ¹½]*ÐÁÏ+‰vãÉfÜשðg‡ê:ta¾¬î{†©Œ´˜†P®In_“ŠÛK}Ȱ¨wé+14ÒÐDœ2Ú¶( #j^GË)âí•`<Ó‘{‡¶ÜV€„«þñöoÔâÉ÷½Ù]Úy)J­Åê ‰´³ö¾Hbw)µïr†lZn–_e…P( Hí‡Är l·¥Þà¦Þ4Ño´¶Ô§œ€ËÚ”,ô9ïaŠ?¦ÊÊöçæÁl[Y£ÓÜâÓEËlSp¾ÀŽ•ïÆ+„\̈•\ÙF BV¼ß!÷ñõ÷[/^öC†…]ÂsmËÕIã ö$2&_§ïf+(â[ÀòlE Öd6Gäç=êÿ/†Èz“¶KýÀîô‘%XÚÔÁºWýÖ¢9wGÌŒ¸ GjûSÂ=XHˆÒóNaLÒbÿ$Q.ËHŸ•A;™Q£]’âÔ…wÀŽÐÆ2:b ¨¤Âñ$¤O^· ©ÿeÞˆ¯»oûi½Å6uÔCxm+ï4[d‚ 4ŒÛ^\4™*D4©–¥P‰›î¿þÓD`I©Ê»Ç¬èV)¸©„ÔÃ~ ËâB!©›â!L>–Èöì)tnj\n¡zh|Ž…f8tô4Ç«¥Ìú^þ£Žï2:—9¡íåä¹YWoTí‰Ú¹ÚÀsúÂøh?û6ÞþTÃJåÍ—©p7ÝÝðÄ(O?éjEÑþ¤¢m»–®eu؃ ôˆyvC—Ü1Q&ÊÀˆb¶ RÝ7ìðz ”Œ4ŸØl§Éq*Ò`ÆÀ„a`×ÜÚ‰… Õhž‡8:‡r@!L)̉!öxŒÆ$Ïİ.³Ü×BL˜‹jÁ\q8ì„È(%)ƒÒ &¶Oº*Rí4zn¥…=°†MÈTÖ#Vn„½=|«ÒnZˆy©ÔÕm¸Ûmö¼H½ä?¦‡’¾v(_ŸpƒýY‹Ö™íÔ¹,ß(Ëz8Kò™ ñs°µ°ó âuzBÅvy4- âà$nk'#vªTd® ñCñp+nĘ/£Ð#ô矆Yhˆ´ÚÂ$ãÒ·¼`ëb½;­Êåhõé5p͈ÎÿZO Ê­ýj­‰ õŸõNµì¾ $æ4ØŠø#=‚ˆ&ö›a+«OÓ tÓ1Úd¦`{üæ5õ–Þ@;IùŠ4»ÎMnùêê“'ŠÅEUIö[ŠÏR›;c‡9*¹ë:DšB·.Øbªµ°Én%ñ`~>6ÕUFuSx8,¬æý’pNâL’@‹×¤ÁÀâ(ûßÍ{n”ó„Áz²¡Õh}¾Žnþ¿´e{¥q¤'½Ÿ'GlbÝÅáv3¥}`¾M¡ÉÌÖ¼o×êýÎ)g•%j‚·P»#c CD.ÖÓ(èƒè?ÿ}ÀäÂE~ IØøK¢v'À+·»Yò> œ¨7é¿=V¯)|4„便È=­]Ó:Œk)³ ‘EÈ.¸ÇׯbêTrPšHbž•føÿÁ,‡PíaÚðœQU°ÜFv‡ i0Xàµ,¸6»ô»|A¢²­±†.à×Z¬}¡éb¡ž€g„a jN~ÓJ ºŠºûh½ÐZ§4 X†Í0ÍmTˆ£›6¸%6h¯¤ò&áb[UYfëuâ'œWSi¶€²7F7lV˜d¯[úêf“)_gÕ,” ° tnšu³LÏÍ&¹•ŒƒÙ’š˜Ox³ôúû30±æ“€i š!Ô I­âAû»üŠuÝjtÆ*©ûñ{Ô˜ ppe/Z1 È„.Êo5f/¶¦—´úÆâ-U'8ó¦¡û]ÌýyMs{qXøy‘é KwA,ÔÝ 0È–.Œwk ¬rK´^} 'H®³YB;%bwÖf¬‹òDšfMŸ'FåˆÜ}âŇ'LATiã™ä)]¥ÓÌ'áb¸açG¹ ˜½Ä‚m –Ô Å99®äòÜ Î†È»A¤GY<¡óÊÎL×´®ípyñ¹e0¼Rëú#…ê2¬ñz¨G®w‰Ë£Oa€HéŸ}@éǸ í Ãù«» K^ôT¼? ’÷a8ÁQ]d”½©S#A«ÖxþøvæøPïÛj4.¢JνqϹ‡²K ˜Ë e$Zé£FÊIdùm®5 ¼œ;OÞíôóGéUP´ó’’ªóš½xµ‚7dt¼£IãúLÖ,ŠêÆÑ°h¢Óá=|Üûö’ÿô¼Óÿ~ö ?vhô†IÅw­Þõ‘´³eEa©1uSÍÿ“oéú܈›ºûï…ä~‹– u=WªÊËNýej/ËÙÝÙ“…ïüð5é&,áXÓÉ[ù›-²–f±pï˜{0 à²^ÀÌCE)”Öy» iHSvh!¾-íŸÁøPkPÂÀУУØ…t©ÑmÍL©®o‘ÝŠt,Ï®Ë@æËà쵓bæk˜ÆN*y真¢ÐWÑAüaVÓ3tŸÈø³ö¹ey<„Ó"\•4×<”ÀíßÚƒG>þ ;¢­'* qÿOÿ]“î ^>©[ƒƒLJA»µú²ä=ÚîÔ‚?ÿW•ˆjPQ}š9<ÃÎDk ru03 óµæ +"¡¨ÓT .ËZ€Eº}Ò%sÆžz ªZQ1À£ũÍÂAêNà2HzJCŠœ6\A¡eeóéO×SßI|ì‹XÜátéiºDÞåŽ >’þøYæBæYã>‹”¬ÝÆ1á¦ëÄׯŠ43¹ÝÏO»d[X̲0 ÿNÖ) ;ß0zá’ÖK£É–܈û9(äüYÌ¡X̪!%ë«ú´æ]lo·h&±µa¬Id­Øå¢nè,GûqŸþ»;òjÙ#8ÜBŸ<33z‹¡m‹±Ãr&@‘ýàM> ¸ÁV" £Dä^ýåê¤W•€áUEþ‘¿ F6«¶ïÃvÆÇ/uÒ/„³akb3u;ø€Ä—76Ša¬µi|[ÝÈn´zSÃ]uIêuÿ#)ºˆ‡Sã°¯KšÂ6¿Zv0ê>ø±™ƒ³Ä4M(ês?`jÉL Q³ÓÂjpK%h–Ùöš×¦úö(U*,X9|hË­8Æ;zÖ1È!Ë(« [‘u Ï2Ô%t †^µR<ç-¿À²†]ƒ1_)êû%$3€VO:o2mȤåË:TZª ˜ìüêˆ#zëÔYd²€­`ªhzÒò?ën>|(‘%NèKôͳA ¼—»¿Nw›QjôP| M7üÑ`ÜÊY¹±‘pÈx^wéÂZW–sŸ!}й§å¦ƒûö”ø£ÀØw×l‘4o» ŽjüºgŸ$Ë/´Kc~½N¬<Ëζgž5§ûUyúþ„™Œ‰ÁA¢o,r–•h‰÷p ­¡ž òǘ­÷{–Âüìód~AlQ!'ªjþ‘±— Ø /m±_Úó9­Öõ,¾óa :§Ã“ŒO%é1,ÒBã Ø~!cÅÐaר¶v:0P]88’\_µç,ý’Ûò¹è•$¦V éŠÕCPªq1 J±å‡Šh–½w¡¬ðÿr8ÿef?ZdzF‚oæ'„’ êñ)‡Yåcä×à^€¿­²¹(¡¯·ZcŸ1Öf$)Ž„¶‘õ¬C‹ ŸøKÁ¬Ôöî ìP…¦§ýþÏ+ÊÅÐR…Up/wß «âMc…ª’,‡Uùü%ñº î#+X¹·§žNZp*¤þéþƒCÁ ‘¡µoÔŽ¡vì$¤þo͸ÕÉ÷®‚u~MšÜ¶| ,ÃÿX{SÞ=®lRF%ap–Ã}|ràÜç½ ×[`B¬¬^JBÈ„ÿÅ÷=î‹Îúð0E¬®|']ç+2(…OUåòŒèܾÚm¨.'ˆ?žý¤jRTHVGvþ–Ðö­•ËQ,Œ°òÉî&ÖSÐL¡²ÿe–ó3~ºÃ9U9SÓÄà^GYF['X-¦R Âæ¯ž-!ï¢q;N¾ªÿz)mÛ° k}@Á èsrµÍ4RùH›÷¶­íŸÒ¼‰±NAÕqúM}µom "X?¢B5ÁYÍ2/qv¬z@ …H‘xOú™Å„ˆÆ©üÊ ßi`ïVA#v¬ÿQÊÃ1W„%‰HÔºÕf¦¥±¸£"'¡N— ÒìÿÿÿXÃÿ×ÿX²§ü£ÄŒ’I­9¾ ³Ð@mÉ$Ë%É$•4·8ß.Z ìsxá ‰.Îç̺pF'¶=•‰ž?:Ç fŽTQ ÆœØ · E£ªÉ²I©ß•"Ôë>ÉE†æx–i&[ðÈawcÿ;`±ÿ3lâIÌÊ:Ø1Sóþ@_»ÃáÊq‚°zq*û‚#úK·;ýɰïÂSKÔ'çk6¦Ú‹I¼µÔ Jeéàµ'Rt5y³ü×Úˆ.þŠÓöN‘©ºÀY¥j÷TmóSÄÓP3ñEª÷Ãh×öP² ûèw¢Eô1À: Ö ¿ü­Í÷‹£ ••$ äN 'y¸•wž‡XÖ'&Ûèwž‡XÖ *×D ¼ðÜ:ƢŠtcu Cç F°ýÀêPo.„ßý0Û¹k¨URqÉò3=Öèè³?'ô{§]Î+/´ç‚ušÁ0…v»À~¶ûK)Âì%€^e8Ï¢6º2Òäv‘;Ì¿tÂw69­ÕaFeqUž²4Ù3=M„†mE´¿Ÿ5Î%"ÏÔ,g—#eÅà6*Ä›òéÂûlêx(÷+ëË&<Éݪf^Ë¢²£”Oð æëÅvÛ뙓wNɺØ)^P^nSNž‹1ä¶k7öN¤n“U@ 1Á[©ÄÁ4ÿÿÿÿÿM3ÐáðšÛ¶„·-uÕ¢åŸÇK’m nZë«D°':Ïÿe½Ü°Φö™ó¹»Í÷(sP•€ù4´ `öyé^iÔJyí_¤Œ_óü6ÛV;I1l/@¦Ázˆ¼Jiô[G±ZtMÁãBìàfªvd òáׄÉrñCª€J†êx¿ñô8ÿìÂv»F÷Û„ ®ÿB à*/‹j¾ÎÁ5WðíRéÊMGþÈH>COs)5[úìŽù4ñG2“QžøP~ÈouÕÿ'ø²Ê¦º§.(²¨6 }r#YçÃrƒÿa&G}Æüÿ%¢:Ù:tHüEz¦IŒQB lÛRÒë’3Þ¶ ªÇ84JMï­(xøÕ»€WRÌs Цèë? Ëâ¾]hÍ33¸rÒBñN¾È¨„á„\|-jêmi¨r«›Ð¡ƒª!•dFüb™]³&ãªÁÌ«=¼WK|p"Éúÿ2¼gÑT†9¨†d»”<õˆ @Å€¥»m²Þ{HoXB@t`ˆ.¨ÚL±ËUx èÙ7hÌjèÀæ9Œ\¢%ox9‹†ýWþ½·ECazõÛ½Ëz›EÎ8ëÿN¸H ÖYe\ùæq˾H%_ÜΙŒ‡ Àj·&”÷Î8J›yZÙDT—ßv¿Ÿ?çDpSPt/Ä(‰Ò0è1‰´)/ÍþΈà¥jRû oœ¦9˜#=ƒ%ù¿ÏùÑ…ï$Ð¥ïX7à‘<.:ë{ZW¥ŽSŸ”Í#™²Þ×Èþü®M@>]‡a•œ58W£òÏ¢B<~]¶TQèf†"{( @ˆ+%P+‹Ó4•ÿ4Á-‡A†¡±–Uxtú¡:Tƃ= B™XÇü£¿ÿLMt«f;< œgæ—  ­Æƒ¦´>œo¹ù^§sÊ!Ô¥ä‡IP‚Ñ1ÊÍ«¿Øê,C{}Šóü4vûuŸV‡ßmcü4Ûø_ÃH¿†ÿa§?ÃA;íÖÿp×Ûí¢§Õ¢—Û¹ÿaµÜßnˆ xÿCMï¶¶¾Ý ¾Û>ûm™õ^<ú´N}V>«~ûuÊûhá¦?Ãw†Ùñ~ÛsöèŽý·'í§¿mÁ}T·í§?mßmÇ}´ß}µ-öéOðy¾Û³økðÔð¦5‰€Ê¢ {Ðä=µ#?T³JñÂÞ+桌â²HûX”xîëÄ#5†è*ÖN3m6®rÎ.”Ð…Éa¢±ÓUBtŽÅœO•ìÈÛXÉŒÎýØÓPMJÏ5AZÎ îÿƒ¼rUU˜^‡.3I¯¬í? ò ÷¡pYI°G$TBúT«RÐ&Üë{q[®3Â>ÝwßîÏëÙóúq°—BSçAÐù˜TéA8yFçže%7îAÆ· ¨“<ª7DÞÕ|•]z¢&cU´YædUܼÒ9@ý¥¢xG%Ǩ¬¤«Q™µXKÆû÷Ÿ«`“cy«Pö#•#šá¹áSøŒ¦w&}b…ÿ‚Yd>ö†ÒÝœÝv‰±ž‰&ØÉuS€3@xÔkɉŠhÝ3WaPU¡¨ÃGçæß—l U.×q´2¦ sÛ4¦‰’êÏé+”5ŽÖ_azxühžPJT{?mî¿™“ð=é«Þ\_ !ÃÚY0ŠÚÿ[‘e`ÆÔÇ™*‡f¯Ìí•ç)„"1¡» ØÂg¯pÁK_ ˆC Îཉê>f‚®•`Ä›ý¤g ÄTçÆ}Ø7,F±¨@´ÎŽ2^ͼLë}á1“hÈÜ‚å\A¤™®Íh­ŸÎ·pÖ‰z>Ø(n=ÄqPè"aánÎ_– -ñ÷Rß¶•mgß5;6Wz^+ð«ÕÏþÍ?*ëEÍá\®ïÕƒæ¸âª[”sÍëOºEsáÆìÞ´ÍýF0»Ñ•Ì¿çd{çâݬ™i½rž†Ê ɉ’&Ù[¥ ¤U£âåÑ*½ôƒ¼ -¿Ï¢ZÄ=Ê–‘Xæx …ƶâñ‘¬ÖÅû|2îwæq#ÔíaRÄ­ËÈ/Ë FëNÂ]8.ë2S}L%xlä€ãG»yÜ€ŸLsžÂo?Ñ$ZÈ"¥åZí7 Pd#½ääº$¼Ö¹.}ZŽd¦tÜöEiÑ p“8Ü{¼V–Å:õþÌ›ñQD$”¼GUJ0››Nïª «%Œ¾z £ƒ'ãqÈõÑáƒ.ÂfÓV•j·¥ñjæz_S&YC[„wX£ ^ƺlŠœDCS#ç\TïZ¤óåÐ*‚UV R_0ŠÎ%Ý6&ó3œ?ˆ3F¢CÕGÈa?÷ŸJ—beü{–Dù¦ô>'Fúãa³.‚¨ÀC¡-)¥írˆ8ÊqrÀ½¯V«ÜêŒÖ&Šœ53m\f™’wÖ;ògI„%ÙÎ’ùŒŸ 9¥uÍb¥|°ä Ê—öèrFRf%DÛíú{J(Å7;Ñ0~ügÌQ×omàS¸säà㼤™òÅWðƒ±äœ®sK›äÀñË%…‚«„eBMõô¾Ô—ù¯*%y…–¹O ±ÙÐë‹R‘0åÛg_í²Ó­Hö¤è»T«$ ™×dÓ~¤¨‘AÅÿh«ŒV)=‘¹°*[‚ywy8N’Øñ.šÚŒÍ”v¨ ™Ÿþð"Åw Åwˆ¾jÕ–˜‚ e-5•°i8cnR‚åTƒþúèííÙ³2¨Öæ»XhxÆ’^<»> ·X圄#ºþÐT×o„·@†H*÷ƒÕšÎÀ.#ͧöN”Ÿþ—·ÙÉR\kÝuFØÚaª±ó°Ú²ý3 %YK3Wü(f ë°b d±°EÕ7ÐhdÚh$‹î>ÇìÎBO\žÛ2s˜WÒ£ÅüÌà,¯ .Dï!Û Z°Ř*^Ðo§‹Ø ç´K§áÅ€§ô"‡é4¬G—X¿OüËG2l¸Á< #YRo¤sYÐâˆm:›é¾œÐÀ ?ëmäòá›c^Qxø"¤e¶ŠÖÖ¹3R/hgŸ1¢Ï"ß´‘•µ·¤Ã<ãnŽoø°'úñ‚û‡^—n{³S©‘I>p¾Ö{Å¡'œ`Š &¹´Bt´.c¿›ñ gP»þ¤3 ú"*(N¦/ý02“p -O+Ôãˆ÷ëSÁ©FéSÅËù‰¼»žÉX”˜g´Åæìr ò.{ÿVëc÷J¦ä‘= 2ì/t3R;Xgö.‰ß˜+Íç-h„ô5t3écï“7a8)Ý;ÍŸvîÀ‚¨ñÂ(]±­TIÙÝ,ѹÌÂïÌffªZ• ÎÈdE=ÂÀë¾Å¶Û¼e:£gñÐÒÒßûŽ!²“¬(0Éëzƒ÷ƒT›G‚,eZè)RÎ9"‘”‰ñ²ªumûP ¯üD'3 ­—³çG­xü[ÏÞ^úU—þìK²_wàfx$c›Õt“R4›…ݪâÌó)µÓ›'Þ ÌÛéÙÑ€0+\ï6j{?Õ8Þ…5®š Æ×–Y@c“H;õ€«>XÔpÝÑ–Ú7âáJŒ`•4;5ÅÓ¡FU<(ø@ÄHÝÿ#uTΣ4k¼ª¶où[VÆí… 6,Ó9XõÓFRÎ7-¼”û ø pVÔ=ʬ¶ôÉú·$9àe™,”ÞåF/˜¶ }N-q*ÂH[Ÿº&5ÌìÎýà9wµÚ©“ýˆBB`Šïn×è-ÃW&¥@}ªU,£ Â8&ì¿–,UŽ™«BYxé/ =¼±ÒñôC<¾ŸrÏ7âé땎èçeжE` ßT ëûØ_™É"9+d²ñF±êàm›ýðÜ%lPEÎ/W3×D&{b/Õo¤z~Ï"sÃþØçàýÂo€zn€â›.FƤ´ ‚«Ñe|` x%›ÔƒÁɳ-½è2[È‹d‰`eU£Ræ§f8HÃOOÜllMëÜ<»‰ ªK<>œæø¡ÊMÖG0l¿$z§9/³3"7¿¤dëUq¼€µæà÷auªË#·Âóˆþü þòKÝ}Ç]ä¸p£;U3¹@GÔO7È~ÏjXZ£‹2^&†d¦;BXó5äÇ_Çmb…«)|É9•áÇ=á(=)’UGzþô±y,\Æ»oßK9(0™Ü bÞæ‹ÃgO>^ c£þßÕŽhåÄ3Žawü›+Uñý,Ÿ\k¢cïiÆføQßù#ÛT…5úµpKÎŒ<”‚©'%µ'¡ 8ŽŒÁ:ª2Ã`=ÿ@Ff Çñ™I¥ÆþN¤©èðxˆæõû{°ƒgG„×-† |ëʺ:³÷]˜”ƒr!AUßÌ c»î• ®n¥y¨«x^;â’eÀS‚MÉÍ3dq‹õä™BÎQÍWPh¢àÁË¥ ŠO|(q8âÁ!Y,q{—)±Ñ`€tÈu¥«6X¬2•žÐ¦!²®Á+Ï¢‹°©åìßV.Ë 2`˜Dç!ä‘=©ã²/ŠV °]XÃÔ¢B% ¶Þ üç(= É|ì‰w zhL”æµ1ŽÀ?̾†5: mÎ7o³­)SÁŽ/AaÜn"Í™¶÷ÿ¸É ¤oûìÿÊ3©—É oBþÞÍ›€¯XÙrC¬ *ÄÔÄAú„o¥\ûŠ,ŠLtze{qc³g,o\,}ƶ»I7äbAÀz”ßrsÀׇü~$Ýuô¾; æ‚9«\PJ|L—+!$ؾÜBâlí ÷Ø”§ÿvÉB*GYÿ,³Ç˜{5„ž3¬' nÉÂ{žwá°»r“ô-t0ôÿn_£1+×G¡7ób•9óY€R‰ÛdÑŽî÷è~» .IÏR*ŸÎÁFÿSæQ…ÀY …½új(Ä4S(`!ä“@Wsⓚ߉$Ù)µtqáѪK€’à*QýƒÏ/ä§Y)`žÙzÊ®£gúm-£Ü¶2•¯ÑúöŸä¤$T¥óIcÃ'˜D÷ç«0€Ù¡ XÂíl³ÌîP8ÁTEX–a<ã$rÃÁ:£]KÒQÏÏÝÜ ÍG#èZU¼@Š5`LtkÌ–KëÑüB d±yn_)Þ6—~¬˜rœëôkÞ3׿ÍJ€U¡ü\w¦ÀÂÈÞR9/æQwBÑ ,ÅèŸb3Småõ'õ’¹±³RÑîü6Šù=w¡×ðЊ>©pVªQN`¬^@RÓ*Áï]ÛTëu¸¶¥Ó``œæ_ùïŒD‹£@ìò@è÷í÷"0¸E» Þ_¥Ô÷ᣳ©­ïðÂ|\ëTÅAm)'«>üÆ ›‰Õ13„½sÖR¡âyÉS»\}B—Óþ}ÎQ3Ìtƒo"dVƒOнG;“€m $ˆéµiôŸþ©êàø²Lñ¨×–z”jŽ»Œ¾¤€íÊžÿe›8úP"nÆÇ›œ2V]c÷ÒLÑ?¹At œ¡Ve‡È½…›i·f xˆµs@XÊÜsí°ó„íÎ ñÖWnʬy ýâÅ~j@¡ýIKŒ(¾(‹PÃ"^£„?)taž( ÿ$Dòž/È´c¬•ðj¼ûÿ•‹ˆ ËáƒØŸ»Ú·½°]P6¢i2Þtœy ÕV®߈ž¸åëwûÁ‹H6Ì#6+EŸéA ,Ù§§¢Ä/M-ÈóËW0Ô{¾¤ÙqíA¡>óÚ¡ð®íîC«×ŒƒyPÔQ|äã‚öÞIžÍŽUO¦÷Hô÷Í0Ç>!M¾mne¾…´x:åq¸— Ö/W(Ͻ^!Y™0“Ì]ØëÏ91LQý7]ŒZ˜ òg倡?5–> rý|ÙQY8#ë}d] ·ªÃklžœCøq'îÓ/ƒöZn6@­än° ½3q/ ¢KBIðùÕ:¡áV}F´÷æÍÇb5ÙG¿[n²hÇcbƒižcÁ‰ú®”  ;qOmGø'Ýaxf¸^úïBñ>Aç)ýb$ºT."<Â'²ÉÈtB²wÙÏÕw%¨šÌ|•Itºà‰ÙBíÝ*[.0{jŽÛY1éER2ÖåWÛjìc=—Fãr˜cnðÈÁQ—vRnbË…ZªÎË®ña/îä N²è7 à¦0 9ô©S^?ËBxqAœá»Eì;¾.zPIÑÍl4=ûªâSžÚ[FÐì±AŠ]1®B¶QÂÁŸ¸÷YæC1¤ü+ÑïïcHvU–°ho+&š¶ªù€8…KNŸ ®øö·ö¡½_Ö/—[×ýÿ…>¿¦ï¨"¥*yuª.pãn@)Ö išØêB’î:jPÇйÃN}YWXÙ̀ʵNœÂ©~+aëÓƒ :›*¸uŽÅS8CÓ<•k?¼6%Ѭc—ˆÞ›lä5*øz{ÃÔw˜æÑ²´¬ ë³Ñˆo4 ŠÜì­pÅlVå.¥˜kå5,®‡„O¡¨y f^gDª×­)f‚–&îgÞ ÿ=˜úÉšl‹£šén4=|¬ß² '¿çÀ¡§>Y{–¶w”°°Ïš‚kJQÙ÷Âe¼uàö2J¿õÉ›ãdóÙP yËcN$zN6Tæ·á@•0`aÏÓy×}¬©ê•‰/GoͶöV“hX¥4 Á(1±àevs,™§L;›ñøÍ:ê´Ïñdâówº¿˜n§¨ua>'6‰ŠÒ/Îø“ߤGœ¹fãFÍj¥óxtl†@쬧gv Çz¡6Å%•-/Lˆ ̈¶&8ÊÄ?Ëå¬gw½ùÏ÷² |¾_P“í£j³]57=¦âìå TsªRÕù¨Ø–‡N’¡¶~«+¥0“ÙÌNU@OÎ’Ã|âÕZ-«£ú;%ZóòRµçáÐ-ÝC­Î{ Ú×ÛtîÕG_i5±?æ…ñ¥ç¥`ÒÖ4[޾½…ËùïvQÃ(ƒ…nûB¬ÀMÇåÐ¥ ÿ[›s ®å½o"&b8¬iµ¶´ Qv?•„¢ºFâa”Z¡“K”e”¡ä,yfÓJß‚/Os÷åã¸"7J\"cmIh•úÖE*ÍâÅKsjòÚò«z™f¡ñY‹µºšr"Íá5e¯C«¡±Ûé3ücý¦¦‡ÉwÂäI"« EÉ>ÿ2Zö`eèå—~fZwµj¡ƒ&€jÎ-êjA=‹Ÿ>ᅥ8¾üÞW —L°»õÉu‰v5›JzéïíZ¢/»±…9Þ´Ý=£ÕØÊšTêAáyî‰P:×XÎvc;>Lj>y:.¿T/YB$çÅ/ÖÕÂkúÀ‚<5 ÿ/J7“æÒY5ªÐÆ¡$†? xÙð %Ü8öa¹¨¿Ms ܱѳßÇz—m\¾ ˉƒª8Ôb²÷f†õµ€JIj+hI¹ÛJn;ûô£QGiqõ{Iÿ(+=:tµGc´Ê»pý>­c¢ãIž¼uq¸a˜óYÑy½ó?-¿a–>Þü5âOhý%nDû§à¸Ð]—**Ã%µÊ 7‚©yýݤ~€v>ƒD¬O{,(“.ý –¯ÛO9©ÉÐ9mWÓ£ìv»ö“ëЇƒÛ¬Í·g*eVÇ"|tªÇG«ã§ºÌú¹ø–¾ÿ9j¿Þ5Í‘k¦Bxh+ð‹f|kHÙüœ‹þ‹Ešv%@¾&%ÎÊ”»†!p,ç¶Û ¸v“B4rʾIp¸^__*´c Â+z¢²xý0|}d¹9‡y@Àê¾lf4„ÝÐ)Œ£9¢x÷~=èU»üM¡ú+ñß¿éÃÉc\»:7ÜŸÌb+N™>ZŒºÄ9á¿&›Ûú5°HKrÍÕ¤›ÜC~å§Ü„šÚè(÷Òl;S(’ìË*9`4ÐcYÔ±‘¥"½ìR~X5ˆ"[û·F¿†ŽN èÛJÙó2ײ˜9u¦hÕ¡DZ¥¾W9[‚4s•,A×éÚB•Õ ƒáåŠx¨m¼mÿmÒ/®í¬­*¹/J&Ùœ;~p©~c½€J aóÀ+Œ`ÿ8µ_CtäØNËÍã‘Ûw¥½Âþ×Ö6RQÕà´ ,ð]äA^@ ¾‡´¢xjÔÒíü»0çø Òƒ6iÜ}ÁÌ,©ÁB̓¦©öVų«ºaèÖS³®þLᶆîâ­N3 zO><ÊÐw3Ù!±Ñ~|¶y\Ü]r°T=<äA0ý¾…&6Š’ùçöÀެ.iû4e è±Îr ÁÇÈtG?öÜ›£Dc>¸#‹]ìmîk\5ì+èBœ¯A@&‚:¹—#eãŠ+Ú¥¥ l"ªi©p†Øìò©šÁƒìlOšjÖc ÿYËšÛÂ%|˜\Û“þ–õQ¯bÓnýW 1] WržÙÌYHݹEò]JÇgFVÔOçÙÁÂU-¾ŽÃEÄnëŽÑÅÜ$ö µ Û=¹^XäòÉÛvˆaHÔªýÇ¿¹Iù²pBâ §MS$+u× ç[„yéé¶Ø ƒ¯Ìq`tÚœ©Ñö1½8?9Rý‹bš{SB\°<‚¼òüœ©í[SKi¡Žö«Y ·pŸyÁ6x²dªôìþ±ÞqZ\¤—ça{G„‹V«–¬‰Xe?·²Uéy¥ew%ü€ìÂÝo®t±¸©<’£òƒú'Sä¸ Ét­oTÑXæ€o\8·tŽ qã 9?xOqri®xžiÓ¢ Ǩ¢ŠNÃT·‹ã ¶ìö*c`HËl¯¡L.›häl–Dò'÷©ô[°ýÝ]$k˜æÇe_÷›žIôi¾5CНâ‡P‡ÝŸi¢%9FX-¯ÑÛ›†+øÚ¥lÙ@ª&™Œœ|i§Év‚k7¼*ÂLwØMl›Ö]š€èêpxÇä‡pS+Ltc¯! ‘¿Qø¥ãP9X,ÏI÷Þ:G ´Zlu¬2íÏÝnîàð÷¡¬gÅ[͸žUÃKn ÏþÒ°A]RåxÕ´P‘"CŽ¡~“Q5´•„â™—kX ËtÌý;º™jq‡li³®âJ];m«·è¹áß¼ š¿ªWþË}îx߈g*Ù°$™2`Ãk\ÜpÚÌÅ»~Ï„‰5ÖbEóL4r„74óau5ÞÙα£”í´Ãêq€rmê ýÙ©*D<÷ÚÁHï™È‰-f.ÊÂêÒÊ Á¥àëŸb@Ù«¦ˆ«çi×´3£;sÅ*¼ºL¢Ë¹1ñ‘€¡ c4l“2¡†Ãói¨à²Çš}î8ÃI!89£B,†=ã2¿æRm•˜ÿ$oϼ¥ ÿ~k\÷S$¼ÓeîÆl Tµ|ø“ qѧ'˜7Ã7£ÞÔ5 „JUýBó\߸6Ù¯qEf{Ø‘è2;cÿ)¾ñ«×B"$šÐlòõœ»*cc2Z=h7ü.ªÖa°äi![Á° z7 “Ýàæ-ÎK3l^ í£11ç& :)Ñ“‡MV,~“+Øæwäý9ŸBÿv,Mý„x§IŒc²)ï\ü,’m¡«9ÀR¸¿,ºô6=±4þSE´Ið2Ã%/!_P¡Õ!%²ÁélÎQ–¯O^,/ÊoG‘ÜpYþlÅjäBRd°KìD¬Œðð|¶‡^k6èt¤™|‚ë™™h0SéKrÕŒ®Yt£Fï:»—J.-¾Îg!æÓÕôTžmìJEט4(0ï<µUfŠq7»’Ud¦Ë©°<>ºÍþÜ`ƒŠ*~oaŽc†WÞ4™¾ë¨¦¶áƒœ[$ ûHÎS݃èW|ÏÕ)H×ùÛ~ìbý¯xƒWŽÁÅüø1J›ð7ï¼{&¡9º¦[þ¦&”RÄÚo²í¼}…7eõ iÐ3ÂVù… I5™DpÏb?€8”ÛðpÞ£¾`Ne¶øõ^ó¼…vJ²>H6ÇÚ…vÇ5øŒlþ½Ê^ˆZléµh¾ÁßËÊdü›5ÓUäøÎ¤%DAró˜†574m Cí´hr5²Ðÿjè¬ ÿB3P˜,Q<:Åá r¥—¡Ï«3}Ìí§?wïˆÆ˜»RˆPQZ²#¥¢™ˆHWg°÷á+ìšöæV»=°7Àlï»×{ \§"püAãO[D $jÄ~…By³ð¾ÆRÀCÎé.È©4P¡Ž1GÊšpM€F¢YÉB¹"ÚÛ÷fb ¦²g&ÙGD$q_ ¬ùBÑ7\| AÙ©ËþBÅšK®i­œsä`ý¹g^U©´nA\…•<5°š/v_ÏÉó¶+ƒÈ­ÒèiëJ):ŽØú}«|a‹ õp³†qìÜB÷ÀÔæw5C„wÏ¡¦Ö{–Ý“DúVÃÿHžÃ :|ª^žæ7q%í*Ln ˆ2XÄFÕýaìs"1’NNZ—*ú­°ƒw´©aJ·âà·>fĆ)£·µuâ¯úxÝÀ׫¾].Ù1¥¡"BÊ`GÝœï»ÁY˜Yê˜üGTÆÃE-Á‚½;h‡ÛŽôËšöW›)™Íø±~3‡´ î†¬<ø¼0^Vú©¿…f|U ^:ÑQ˜4ƒ&™ s-\ïÊ3¥™!*ÀŒ¦:ø£±L«Ü•S»ÐÇ6o†¡ö~îNü?[ƧG(Y[ûb£>ÕÉ"÷;(ŠRäëeÀ?<׸¹mÈ"#™{¯(åçaº t´ðÐ{ðE¥SD–$køooë.zÔ3¥6o Üó[;Y~²Dþ¢¡kж4!ïV8pË̓¸J+AÔËû³2ªæ{Â#<­‹µK’µˆª&âuP¯cÁÈËd—ŒÀºä¿­Ö€˜`wr á¾@®V¬0#@úï:Rz¯{‚$¤gQí[ÕG´¦èm÷&GGg{×Ò„‰‹×óÚ\A°"—÷áÕ×Åû3á$ø:EæAûx°Û¯5K6¤&„mICÍW LÒüêO§zùåMP-<ô&rzW÷Žý½›qv Ñžþ.uòe¥îüQa¾ÍçÉN‡òô{  h{8a·3ÎÊ\‡¡Sòl°_ø«Ur'MYVÑ.‡-ÃáÄÞ |°ñ\¨è`øÀ66¬Òì§åÉm݇±)‚…€<–ÕO;^Ö+&yE]å˜sƒÇr.bn;-nÜ%@ƒÌ}ÌšýJÛÖ]¶ߨ€vÛbJêË6VrPʸa˜>SMÂH1`×îSñˆTºo„ ÚÕ$üRãôΞ,ú¬ ÍY >{ï貆SàŸPn´Þþ]¹½ÔÈúqi©L“ ø\´r:{ÿ .-ìò:{^΄o¾gPŒÁ¯`n+”¦êæJ΃—odÞ”TÛ—hH¼³’EËóe…:^ƒ2ö¨­VJ6¹Çr@®Øâ!ûPL÷© ºÌüÓ5(Aˆø!™ÉpSûІe¨G'Ù’»ÒÁóÔüdÄï;PÙÕàæ¨y:«ì,0¦pQ‘ÃdïaŽž¬Ç7ÿ^«j´dîÂb¤–¬­›0÷ð.t0cÍ5hh°Âðà±lïaTïÛŠ£ÉíiŽ,Q«vÏ&fS_¨LQþ:æ£ñÇzûTLh(éu1b\ëF£\˜Ho¦«â§Y"‰¸kK†ê1-IzÐnxyPÜ(GªqK& -ÿE–´Ióá‰ÿ?®ÚûÄìïî ÔPe*:Z¿˜d”– qÔþE ~dÖ\ã•™£ôûÝ_AYÄÄÌòOø÷…2pº8zÚ½Ÿ¾.}-¢ R¬µ»óó•[(¦ƒî;l~=:êsQˆ¤`o¼IÔc«ŠKo£ÂB·(CŠfè×NŸäM‡í}{™5G¶ëÔz8&kñˆAH·z@òQeDÍ߯]O²~¤mÃö|1/‰ªšUyìγÛ/êËz¢À¥Æ xòõ´Ò tI‚ ¼ØÌ¼—ƒŸb0L&a èû ­¼°Îõw™Ûn.[ôdÑŒ]óMÕõí"Õa÷IGr:Ãëiv4ƒ¬½8ÂÚ€ÄÝlH6´Y ØËa÷×aå~‰ {E¦Mt–Ï£(S?˜ÐމåǬ.݇ÿP 8Y¬ùªf¾í]™=ÓÝîýÛMù¸Í У֬Ç1¿¦?ê'[_Íl>ýϺGýžñA¦°›£ éÒÏKpëÂ[ã‹O(uåÙ½Íò ‚ò2X‹ÇÊŽcJÄÑ0ìE° zúÁ£òPêw|O¶ÙEä®'I­›§0~‹Òï} ëñ4H ‚µ‡ÓHU=ðîý\\”±×™ ñøwæ¤})A¯òÈ©5Öº%{ ]YnÙE¦õ¾ã#‡¸#Wòó!°Ã)„0#,IÈŽmŠ÷tÈàv£¹1„QO¾z¿kƒF¾ä¥°ÍsBæÐH‹ ü%žU©'Qg¾Lë-¯Í†1±;R'cìQbSmKŸÿIHîœ{±ÌÙ°ËÙ®ÝïÕ§':Ÿ„곡óY÷µŠò3UÐâ†& f`£nðÉÝÇ8€:Š|ï´få.¨¿¡A8„QƒYŒV)·}ƒª|3q’I…UFø¿nÕ,°ãÁè:c XZC‚5â6åó÷u¥„¢=t ]*Ò]+ˆwvçø‚Avy ÔÆäσ­´äH¬î©ÐÓ[ä#`%VKæ=çòµß¹(Óç ìÎpótåâ·…±‰/)ð¹ÑŠiIåÝ6R‘åðø—l©X³õ4+ßXáx±q‚,ÏþËË”£ÛÈ'ÿtë5ȳX4bzÈ’äü¿¸‘úÄÈ3?øPB«ÿ@1–(ùmÓ A9ÕΚðoÉ „Þ.©”Ë™ bœû )Ó—í;´†Åº×RøÞñQ÷ÐgVÕNé'§ÿ&&2>YÈ/¬¡»q±ú ~-ã -îÖm0¥Áaz#bã– Ú‡Ÿð½·9èT·ìFÈkº[.püe¨ÿW³ÕÝ-þP+RÃ|KHF+›4üLG÷K4òŽ@ >2B "^²4`4í·D÷Í"Gl[£¥‚§PÈN…¶S}~eñíõxö5¾¦ã Ìámç‚~vLé:MÕˆë‹Z™qýàwùÙ”Øé¯B¾Ì­L§è7a/MR3$̾S¢®ÿ+V¥ÉÓÆˆ¤—ŠÂ꫄ꔫ¥-kTʾ ÿ=‹ÔÛ|ÜOu4ÆŠY¢¸·‡Fš Œò”kv?ý#Pÿ»\yýö%é´ÅçÛ=Öwϲ/Ø, PÍê„{Â2]Ô©%º™T²3“ªììè ÆBü^ݳ–1ò–ù`á9¬‹Ö¼þk¶˜$¡`vÊo5£²üÕ¬öÝ¢\Ê`®·duBÀ"ÌàsÇœñKq¬­A $zfÅg?"“и„%£’hrŸ5Œ¤M·¸Ú½ìo^éûkÔêò–ì•ØÆ÷‡ÃÊõ#’Ǿ,F˜úI†WÅw®­÷ 2r qŒÿ.’#e`b;6SšgEûêß·q¡EX.þùv,ýD1±ùÿxþÎ]¥|—õ2ÃD¤Ç¥NN-ÂÁF~jáÊ™QŒS:^U5âY ŠÏ×î¾­ÑEAè“=×ô*/øuÚâ“Fg¸æVËûÊË’í0K³[†/>“ûÂDŠ1C“,  ?æ#@ÞyuÔ¾–Ñ)¡kçŒmÀyEûCÆðȸÑd"k\€$ðÄdñß1°fÍ›,ÆœþS”íÎ)€v(1Ÿze•“'±bùüæ£ëš±=ÃÌC [Ç ÙÞS¢âÿSæP¥®P?ú%Ã|ÑT+oHú@­3^ªvO#‘…yƒ§å¨Cv®FÊ”q Hn;X½ «È‡öW8 2ÿ7¼(VʳÈÝ)®ýH88°Xvîî¹âTf«ÔKW¡î›7g_¡cïŠÅ–ÓØ‘œº¦¾È1ãŒæ2¦o-@ceˆ± Uÿ«k¶væáìHHlÁ˜˜wÏoaL% {­ÖWg5£óçþ'&Úl5–ÑãЀ+WyE(R÷f«kèð/Nª‰þ¨’£‰¾ƒ• X¼® /²ß#šqpX?Î…Šgˆ+¾„²‹¥/Åß´6Ì–"M¦¯;5 æL1`ÆÝßL—}²–+¤îz+ÏOHM¢üÛîÝé9Ö¹ÕÂeã,‰ÃJFb{}EGO{fDZŸ0Õ8ÔÜuŽ,zm¼L¡·pó;0{U¸RÀûÕ ¯ù,ƒœWW¿JÕ†’¦A³åRsõÜy‰øý†@-¯«DèIB1ºÈ–R)#³8Ø‚˜ qç‚Zõ$ç_|œ¾E¤f;.>ñvù™ó%R† Yf‰€tŽeîÒÅF±ò­V Óè ¾ik¾„ƒµŠÍ¿þoÜfÅzék ›'é’ ÈVvﶈ‰<©·.ùˆ?ÒéÔö½¿õ:¯*l€HœÃÛêÞÑœJ#p– ÃVç­‹VáÚ^%¡slêôíÃ’ý»ÞQ¦ÿ+ü/ìî…óg`Ïæm“Ûôíç³ÿõŸgʬfçÚܿ/—ÉŽE˜îò£ ©1>Uº”GDê;U#’CÏ¿U‡A¸Õ¤ŽÀP žyFò8%°©+ïPûn ÚQøÙ»4²e`ýÜžò”ZIÞ%… ±FIצE~c5^ÃjpéÈ ]ÓÏ«²ŸË“þ½¦>Å@ÀÝo7{óbZêxÀV[t­xÎûÝÜÓ++È.C«þÚ¹eñ\†Á‰]të|Xš…*×ƒÝ Gºû„\1ç ¾$,²l Š¥ öކA §ÈïyÁ–óÞ¡^.ÍœUòêØ “€ÞKù-yP‡Ë[g–± oV< T8ÛJ¤_½Š3úUÞ= pÞtT?ÿÁuŸŒú|¤©yJ+Òhi‘W@ÏZëEÉðµ:>$ ¯‘õN=ùßÃ2é Ìø¹. )# âÙvËÿÙicnV Bþffmolequeue-0.9.0/molequeue/app/icons/molequeue.ico000066400000000000000000000732041323436134600221250ustar00rootroot00000000000000 hV ˆ ¾  ¨F00 ¨%î î1–D(    ) ¿) ¿) ¿) ¿G) ¿[) ¿P) ¿@) ¿fÿH™&H™xH™™H™X) ¿) ¿) ¿() ¿) ¿Ù) ¿÷) ¿ý) ¿ù) ¿î) ¿Ì%ÓIœ5H™æH™ÿH™™) ¿) ¿) ¿K) ¿Ø) ¿þ) ¿Ú) ¿ ) ¿ƒ) ¿¦) ¿ú)Àå4L…SI›²H™ÿH™ÝH™œ) ¿) ¿>) ¿ã) ¿ñ) Ã}* Í) º) ¿) ¿:)Àt--¸>G“)¨H™ÿGšÆEœ -H™ ) ¿) ¿¼) ¿ø) ·p #F , & #?!-"  H) ¿‚) ¿´) ¿Í) ¿Õ) ¿Î) ¿´) ¿™) ¿Ø) ¿72C‘H™H™ŸH™ðH™ùH™òH™K) ¿) ¿) ¿B) ¿²) ¿ò) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿p-1¨H™H™H™ÿH™ÿH™ûH™e) ¿) ¿ ) ¿y) ¿ì) ¿ÿ) ¿ÿ) ¿ú) ¿á) ¿Å) ¿¹) ¿Ã) ¿ë) ¿ÿ) ¿ÿ) À°:`jH™H™÷H™ÿH™ûH™öH™€) ¿) ¿ ) ¿Š) ¿ù) ¿ÿ) ¿ù) ¿¹) ¿[) ¿") ¿ ) ¿) ¿) ¿Â) ¿ð) ¿Ã+'µ}F‘)…H™öH™ÿH™ýH™šH™[H™p) ¿) ¿) ¿o) ¿ø) ¿ÿ) ¿ê) Án* Ì ) À2!þ) ¿) ¿") ¿e) À3ÿ IœAH™íH™ÿH™ýH™žF› K–H™ ) ¿) ¿3) ¿ã) ¿ÿ) ¿í) ½[  $ # # #!* .J!%G•‘H™øGšŸK–ž=X„O) ¿) ¿˜) ¿ÿ) ¿ý) À|!: #§ #Û #I # #A #× #~ # # #½ #Î(9":JœVui?ŽN‰O’OO) ¿,) ¿à) ¿ÿ) ¿Í) Ä #Ä #ÿ #U # #‘ #ÿ #Ñ # # #Þ #ó#5ækOªOÿOîO9O) ¿k) ¿û) ¿ÿ) ¿{' › " #Ä #ÿ #S # #Õ #ù #ö #O # #Þ #ó #5U6OcOüOÿO|O) ¿Ÿ) ¿ÿ) ¿ô) ¿@( ¥ # #Ä #ÿ #Q #R #÷ #› #è # # #Ý #ó #6K2O"OßOÿO¯O) ¿¼) ¿ÿ) ¿æ) ¿%' › # #Ä #ÿ #Y #  #é #3 #± #ß #> #Û #ó #60&O O½OÿOÊO) ¿Â) ¿ÿ) ¿á) ¿ ' — # #Ä #þ #z #à #± # #g #ú #~ #Ú #ó #6&"!OOªOÿOÓO) ¿²) ¿ÿ) ¿ì) ¿.'   # #Ä #ý #¼ #ø #e # #' #å #Ì #ä #ò #6(#!OO¯OÿOÍO) ¿Š) ¿ÿ) ¿û) ¿V( § # #Ä #ÿ #ù #â #& # # #© #þ #ü #ñ #69*OOÊOÿO¶O) ¿N) ¿ó) ¿ÿ) ¿Ÿ+ æ  #Ä #ÿ #ÿ #§ # # # #^ #ü #ÿ #ñ #6T6O6OíOÿOˆO) ¿) ¿Ç) ¿ÿ) ¿é) Á: #Ä #ÿ #ü #] # # # #! #Ý #ÿ #ñ #6 (OˆOÿOôOEO) ¿) ¿j) ¿ü) ¿ÿ) ¿µ&“"m$‘#ƒ # # # # #g #” #‰%‘Q8OåOÿOºOO) ¿) ¿) ¿¸) ¿Ù* ¾pU5n—S‘Q4‘Q&†L›UOOO$OÃOÿOðOMOO) ¿) ¿( À"1$±$PpO¿OÛOÖO7OOOOO:OÄOÿOüO†OOO™UO›OÿOÿOäOkO&O OOOO9OŒOçOÿOûO™OOOOOtOÿOÿOÿOüOäOÃO°OµOÎOïOÿOÿOíO€OOOOQOÐOŽOÖOúOÿOÿOÿOÿOÿOþOìO¬OBOOOO,OPOO"OeO¢OÈOÖOÑO¶O‚O=O OOÿ€@ü@ð@àÀÀð€DD@!     !8!€8€ƒÀ~ðððð( @   ) ¿) ¿) ¿W) ¿-) ¿H™H™H™H™ H™H™$H™8H™9H™) ¿) ¿) ¿ ) ¿*) ¿T) ¿w) ¿‹) ¿) ¿‡) ¿n) ¿G) ¿4) ¿É) ¿e) ¿H™H™H™|H™¼H™ÎH™ÞH™îH™ÅH™) ¿) ¿) ¿) ¿f) ¿·) ¿è) ¿û) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿÷) ¿é) ¿ÿ) ¿£) ¿2A“H™H™3H™ÓH™ÿH™ÿH™ÿH™ßH™$) ¿) ¿) ¿ ) ¿a) ¿Ï) ¿ü) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿Ø) ¿+'¶H™H™IH™ãH™ÿH™ÿH™ÿH™ìH™8) ¿) ¿) ¿) ¿¢) ¿ø) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿þ) ¿ó) ¿ç) ¿ã) ¿é) ¿ö) ¿þ) ¿ÿ) ¿ÿ) ¿ø) ¿L-0ªH™HH™àH™ÿH™ÿH™ÿH™ÿH™õH™N) ¿) ¿) ¿)) ¿À) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ê) ¿«) ¿h) ¿<) ¿') ¿") ¿() ¿p) ¿÷) ¿ÿ) ¿ÿ) ¿õ(ÁnIžCH™ßH™ÿH™ÿH™ÿH™ïH™”H™ÒH™j) ¿) ¿) ¿ ) ¿À) ¿ÿ) ¿ÿ) ¿ÿ) ¿ô) ¿ ) ¿5) ¿) ¿) ¿) ¿) ¿) ¿•) ¿é) ¿º) ¿|)À?B?VH™ÝH™ÿH™ÿH™ÿH™òH™mH™H™1H™C) ¿) ¿ ) ¿¢) ¿ÿ) ¿ÿ) ¿ÿ) ¿ä) ¿`) À) ¿) ¿) ¿) ¿R) ¿,) ¿ H—#JŸHš¦H™ÿH™ÿH™ÿH™óH™pH™H™H™H™) ¿) ¿) ¿c) ¿ø) ¿ÿ) ¿ÿ) ¿ä) ¿L) ¿) ¿.!Ä/!Å- Ä- ÄS±E‘Iš*H™ÃH™ÿH™ôH™rBŸ!CŸ!OO) ¿) ¿) ¿Ï) ¿ÿ) ¿ÿ) ¿ô) ¿` #n #› #‹ # # # #R #œ #` # # # #ƒ #›#uAƒ 0H™·G› ue|ŽN:OdO O) ¿) ¿h) ¿ý) ¿ÿ) ¿ÿ) ¿ž* Ê #¹ #ÿ #ê #+ # # #¿ #ÿ #Ñ # # # #Ü #ÿ #Ê#F¥ †W 4O›OìOïO@OO) ¿ ) ¿¸) ¿ÿ) ¿ÿ) ¿é) ¿5( © # #¹ #ÿ #é #* # #< #ð #ÿ #ø #Q # # #Û #ÿ #Ê #sD O`OúOÿOÿO•OO) ¿4) ¿ç) ¿ÿ) ¿ÿ) ¿©) ¿$m # #¹ #ÿ #é #* # #† #ÿ #ý #ÿ #ž # # #Û #ÿ #Ê #`;OOÉOÿOÿOÕOO) ¿e) ¿ú) ¿ÿ) ¿þ) ¿e) ¿ $ # #¹ #ÿ #é #) # #Í #ý #¸ #÷ #Þ #! # #Û #ÿ #Ê #%!"OO}OÿOÿOôO@O) ¿) ¿ÿ) ¿ÿ) ¿ò) ¿:) ¿ # # #¹ #ÿ #é #' #I #ø #â #C #Ò #ÿ #a # #Û #ÿ #Ê # #OOBOõOÿOÿOgO) ¿¥) ¿ÿ) ¿ÿ) ¿å) ¿%) ¿ # # #¹ #ÿ #é #, #• #ÿ #ª # #“ #ÿ #® #" #Û #ÿ #Ê # #OO#OâOÿOÿOO) ¿«) ¿ÿ) ¿ÿ) ¿á) ¿ ) ¿ # # #¹ #ÿ #è #I #× #ÿ #a # #J #ø #ç #J #Ù #ÿ #Ê # #OOOÕOÿOÿOO) ¿ ) ¿ÿ) ¿ÿ) ¿è) ¿)) ¿ # # #¹ #ÿ #æ #† #û #ß ## # # #Î #ÿ # #Ø #ÿ #Ê # #OOOÔOÿOÿOŠO) ¿ƒ) ¿ÿ) ¿ÿ) ¿õ) ¿B) ¿ # # #¹ #ÿ #î #Ó #ÿ #£ # # # #Š #ÿ #Ú #é #ÿ #Ê # #OO OàOÿOÿOxO) ¿X) ¿÷) ¿ÿ) ¿ÿ) ¿u) ¿ ) # #¹ #ÿ #þ #ý #û #Y # # # #B #ó #þ #þ #ÿ #Ê # #OO=OóOÿOüOXO) ¿() ¿Þ) ¿ÿ) ¿ÿ) ¿») ¿ & ˆ # #¹ #ÿ #ÿ #ÿ #Ù # # # # #Æ #ÿ #ÿ #ÿ #Ê ## "OOtOÿOÿOëO/O) ¿) ¿¦) ¿ÿ) ¿ÿ) ¿ô) ¿L( « # #¹ #ÿ #ÿ #ÿ #› # # # # #€ #ÿ #ÿ #ÿ #Ê #X8OOÀOÿOÿO¿O O) ¿) ¿R) ¿÷) ¿ÿ) ¿ÿ) ¿») à #¹ #ÿ #ÿ #ù #Q # # # #: #ï #ÿ #ÿ #Ê #qBOYO÷OÿOÿOrOO) ¿) ¿) ¿¸) ¿ÿ) ¿ÿ) ¿þ) ¿‚#] #R #s#q#f # # # # #_ #s #s #Z-ŽP OÌOÿOÿO×O"O) ¿) ¿) ¿E) ¿ì) ¿ü( ÀÂ%ÇNèP*ˆÓt¤YVPÅgOOO¥OÿOÿOüOpOO) ¿) ¿) ¿j( Ái^9^(‘QFOmO“O¸OÀO6OOOOOO OýOÿOÿO±OO) ¿) ¿WOMOôOÿOÿOÿOOOOOOBO¿OþOÿOÿOÎO*OOOO4OïOÿOÿOÿOÒOuO/OOOOOOOLO¡OíOÿOÿOÿOÐO7OOOOOÙOÿOÿOÿOÿOþOêOÇO©O›O O¶OØO÷OÿOÿOÿOýO¸O-OOOO O½OùOÞOûOÿOÿOÿOÿOÿOÿOÿOÿOÿOÿOÿOÿOâO|OOOOOOšOO"OkOÀOíOýOÿOÿOÿOÿOÿOþOôOÏO„O+OOOOOPO(OOOO?OvO£O¾OÇOÂO¬OƒOMOOOÿÿóÿÀÿ€ü€øðàÀ?Àÿü€ €  ÁÁÁÁÁ€Á€Á€ÁÀ€Àƒ€ÀÀþÀüèðøø?øüÿüÀÿ(0` $  ) ¿) ¿!) ¿a) ¿) ¿H™H™H™H™ H™H™) ¿) ¿) ¿) ¿ ) ¿) ¿) ¿ ) ¿) ¿) ¿) ¿) ¿) ¿w) ¿À) ¿) ¿H™H™H™H™&H™6H™KH™aH™yH™’H™©H™ºH™=H™) ¿) ¿) ¿) ¿=) ¿k) ¿”) ¿±) ¿Ã) ¿Ë) ¿Ë) ¿Ä) ¿³) ¿—) ¿o) ¿>) ¿8) ¿Ù) ¿î) ¿4) ¿H™H™H™MH™ÒH™óH™ùH™þH™ÿH™ÿH™ÿH™ÿH™bH™) ¿) ¿) ¿ ) ¿?) ¿Ž) ¿Î) ¿ò) ¿þ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ó) ¿å) ¿ý) ¿ÿ) ¿n) ¿) ¿H™H™H™QH™ØH™ÿH™ÿH™ÿH™ÿH™ÿH™ÿH™zH™) ¿) ¿ ) ¿L) ¿±) ¿ð) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿­) ¿) ¿H™H™ H™ H™ÿH™ÿH™ÿH™ÿH™ÿH™ÿH™’H™) ¿) ¿) ¿/) ¿¤) ¿ó) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ß) ¿#) ¿H™H™ H™ƒH™øH™ÿH™ÿH™ÿH™ÿH™ÿH™ÿH™ªH™) ¿) ¿) ¿a) ¿Ý) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ú) ¿V) ¿H™H™H™€H™÷H™ÿH™ÿH™ÿH™ÿH™ÿH™ÿH™ÿH™¿H™ ) ¿) ¿) ¿†) ¿ô) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿þ) ¿ï) ¿Ñ) ¿¯) ¿•) ¿‡) ¿‡) ¿“) ¿¬) ¿Ñ) ¿ü) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿—ÿIœH™}H™÷H™ÿH™ÿH™ÿH™ÿH™ÿH™ýH™òH™ÿH™ÒH™) ¿) ¿) ¿—) ¿û) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿÷) ¿Ä) ¿x) ¿8) ¿) ¿) ¿) ¿) ¿) ¿) ¿) ¿_) ¿ù) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ö)À¨4L…HšzH™öH™ÿH™ÿH™ÿH™ÿH™ÿH™ÿH™²H™IH™¿H™áH™)) ¿) ¿ ) ¿’) ¿ü) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿û) ¿À) ¿V) ¿) ¿) ¿) ¿) ¿) ¿») ¿ÿ) ¿ý) ¿é) ¿½) ¿€)ÁB.5¤H˜ xH™õH™ÿH™ÿH™ÿH™ÿH™ÿH™ÿH™¶H™H™H™H™‹H™6) ¿) ¿) ¿w) ¿ù) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿è) ¿w) ¿) ¿) ¿) ¿) ¿) ¿[) ¿Ö) ¿§) ¿g) ¿/) ¿ xÿK¦ HšsH™õH™ÿH™ÿH™ÿH™ÿH™ÿH™ÿH™¸H™!H™H™H™H™ H™ ) ¿) ¿) ¿J) ¿ì) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿Ô) ¿G) ¿) ¿) ¿) ¿) ¿*) ¿ ) ¿) ¿) ¿H™H™H™“H™ýH™ÿH™ÿH™ÿH™ÿH™ÿH™»H™"H™H™) ¿) ¿) ¿Æ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿Ð) ¿5) ¿) ¿IœIH™—H™üH™ÿH™ÿH™ÿH™¾H™$H™H™) ¿) ¿) ¿|) ¿þ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿Ü) ¿;* É # # # # # # # # # # # # # # # # # # ## # #Ø #ÿ #ÿ #û #R #OOO~OÿOÿOÿOÿO½O O) ¿z) ¿þ) ¿ÿ) ¿ÿ) ¿ÿ) ¿à) ¿ ) ¿ # # #  #ÿ #ÿ #ÿ #˜ # #É #ÿ #ÿ #š # # # #Ö #ÿ #ÿ #ˆ # #Ø #ÿ #ÿ #û #R #OOOjOÿOÿOÿOÿOÇOO) ¿t) ¿ý) ¿ÿ) ¿ÿ) ¿ÿ) ¿ã) ¿$) ¿ # # #  #ÿ #ÿ #ÿ #• #H #ô #ÿ #ø #P # # # #– #ÿ #ÿ #Í #1 #× #ÿ #ÿ #û #R #OObOþOÿOÿOÿOÈOO) ¿e) ¿û) ¿ÿ) ¿ÿ) ¿ÿ) ¿í) ¿1) ¿ # # #  #ÿ #ÿ #ÿ #š #’ #ÿ #ÿ #Ò # # # # #L #ö #ÿ #ö #i #Õ #ÿ #ÿ #û #R #OOgOÿOÿOÿOÿOÂO O) ¿K) ¿ó) ¿ÿ) ¿ÿ) ¿ÿ) ¿ù) ¿M) ¿ # # #  #ÿ #ÿ #ÿ #µ #Õ #ÿ #ÿ #‘ # # # # #Ï #ÿ #ÿ #µ #Û #ÿ #ÿ #û #R #OOOyOÿOÿOÿOÿO²OO) ¿-) ¿ä) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿z) ¿) ¿ # # #  #ÿ #ÿ #ÿ #ë #ù #ÿ #õ #I # # # # #Œ #ÿ #ÿ #ò #õ #ÿ #ÿ #û #R #OOO™OÿOÿOÿOÿO˜OO) ¿) ¿Æ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿³) ¿) ¿ # # #  #ÿ #ÿ #ÿ #ÿ #ÿ #ÿ #Í # # # #D #ó #ÿ #ÿ #ÿ #ÿ #ÿ #û #R #OO OÂOÿOÿOÿOÿOrOO) ¿) ¿“) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ç) ¿/) ¿ # # #  #ÿ #ÿ #ÿ #ÿ #ÿ #ÿ #‰ # # # # #È #ÿ #ÿ #ÿ #ÿ #ÿ #û #R #OO/OèOÿOÿOÿOõOEO) ¿) ¿T) ¿ù) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿) ¿) ¿ # # #  #ÿ #ÿ #ÿ #ÿ #ÿ #ò #B # # # #ƒ #ÿ #ÿ #ÿ #ÿ #ÿ #û #R #OOOpOÿOÿOÿOÿO×OO) ¿) ¿) ¿Ô) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿Ø) ¿$) ¿ # # #  #ÿ #ÿ #ÿ #ÿ #ÿ #Æ # # # #< #ð #ÿ #ÿ #ÿ #ÿ #û #R #OOOÂOÿOÿOÿOÿOœOO) ¿) ¿) ¿‡) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿‹) ¿& “ # #¡ #ÿ #ÿ #ÿ #ÿ #ÿ # # # # # #À #ÿ #ÿ #ÿ #ÿ #û #R #OOO[OøOÿOÿOÿOöONOO) ¿) ¿/) ¿ã) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿í) ¿M( µ  #˜ #õ #ñ #ñ #ò #ä #: # # # #v #ó #ñ #ñ #ñ #í #M #OOOÈOÿOÿOÿOÿOÁOO) ¿) ¿) ¿‰) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿ÿ) ¿Ó) ¿(* Ó ## #9 #8 #8 #9 #1 # # # # #7 #8 #8 #8 #7 #E/OOŒOþOÿOÿOÿOùO`OO) ¿) ¿") ¿Ð) ¿ÿ) ¿ÿ) ¿ÿ) ¿é) ¿’) ¿) ¿#ÉÿÿÿÿÿÿÿÿOOOcOóOÿOÿOÿOÿO¸OO) ¿) ¿) ¿W) ¿ò) ¿ñ) ¿£&Å6 ÿ2#­OOO!O1IDATxÚíy¸]U}÷?÷Þäf $„€É…L Á€¢ ƒ¨¡N§Ú:ûúÖÖVkKßê[kÕê£Ö6µÕ:€u¬´ZTD˜ä"†0Ɇ@! ™§;ßþñ]ûÞuOÎ=÷ k¯µö>ûó<çÉMrÏÙûì½×wýÖoý†rDûªÕSIÀöž+. }JQ3.ô ¤À)À*àÓí«Vïk60"Ø ´ û[í=îúA` Ù®[³Òú\bþwÿ | ¸ÌÃÃl¾@;²p&3€9À,àH`&0Ýü9Ù¼¦3ö}vÝÀAóó^óçN`° Øc~çÐCN®m3“°È'OO¬Èʃj}‰h93è0¯ã¬×<4Ð' QhÚÐìï‚dô˜W7„-À3ÀSÀ&`ƒymö]ëÝìäm М`~>‰Á{Ñì%fÀC³ù1ÀR`1p¢ùs0ÍæmO­Õ¼Æ›ãÌ5ç”Ð,†Hn_µúóo{Ñò¢…É›pp°Âüs?ðà³@  5à@ƒü`p2šåg¢Ù?Ët¡åÂFà~à^ànàQ`Ðý(ÈŸÜÌ·þk²~þg!s^-h}ð2àåÀ©ÀB4óûœÙCÐ,'€;Û;ÐÒá…/!y[t §—Í´xXïã$¬A?X‚,’³€™ói}¡<Ó†,ž›×{‘ÏàwÀà·í«V?„¬†B <’7 à}À7кÕfíüp0ÌrÞMGëøó€W ÓþÜ9çòÆZÜ\üHÄ ð¤L.À|Ÿ>>ʯí>| Ü=XæØíÀ"àUÀùÀK(}=$bpðKàZäTì.„ ò$€oTáWE[ƒwCý"`™øGgçÇ’¿eU(úÐ.Âo‘c÷Fà9Š%‚Sòô°NANÀJœ|x? n© ˃¿xp°í׸eræ¾xÚI¸¸²}Õêõ@o!“' `ЉfáJô¢@¡¤Ê­AóùÑ` 5^@¾4 ô£à£Ÿ?BK…®Bê'Oð*àǾ PŽ­(døj})`>wpð.àÕÀÑääºe˜A´½{ðà6´X8 k$O3Ø G¬ÅÑhkðQàÉÒÿ,øïC޽#CÁ‚!ZÐ=|;ðdù}¸¾}Õê}PAµd~&³v¾|´†·_1ïéê¹â¢bàg›}h×àë(¶àP!c“˜|xkoßüðŸ(vàÅÀ£5~1ð³ÉNà(ä. §‚ÑÉ‹üí¿×ÊCÀߢðÜw£„œ‚ì³ø!p1ZêÛ‡eÈ‹œˆÖóêøˆA$ä;Û® }Ñà¿ø/äü-üy‰T;˜Vç{[Ì{‹ÁŸ?Z€ ÿÐQìÆDËoÔôäEžOõ;ÍG;pÚ)øgàÄöU«)„ ã`ÝÀެ—/|Å‹¼˜Úì"‡A3™±C€ ZP–¯¢mà%Íl äA¦0\¬  Z¦ïA!ÅH“úò G¡Zuõ° Å |8®Ù¬<À Ùx1ø v6ÐÚ,"Y°nP±Pà‚V´Sð= :¥D ³`hAÀÌ4DôUø`^Þ—YÏ,vgÕ<èE =’æƒæµÓ¼å?Ép+±vósÒhd<ÙŸ$êa Ú.|>ê0uoûªÕ¹Œ ̺L£9wÐ >€*=<âß·›×n”Óe~'éû—ô„áÁÞb~>>9 ÃQ¨3Ñ1ÈÑ:ßü}ŠyåYZQ6è<àÿWµ¯Z»ž‰™5Yv *ê1;ôù¤Ì!4 G±íëPý'P|ûó;N;ðX¦ïxTsq2êCØ*0-^ˆr1ŽD¢ÙgªÏœ„¹Ê.ÌìÍ2çÀÈŸ° Í⡦w¢þ´Ú²ºOE–ÂbÔ÷àTóç<$™}ÆJجFgsÓu:“7Çš™.B›¬“d$>†Ê[݈ý³æß£OeµîÉ$T­çd”b½5GÉC™ôC¨¬ü?Ûc¿'Õehþ øóÐçÓûÑ, p3ꔳƒôγîÑ d$R^„Ä “ÏòŸ\ŠúOl†l§gò&˜‡k ãüýÐçS#½¨EÙµÀU¨G^®ëÝ—´J; X‰îÛ ‘Å5+£¨™, À\ÔJjIèó©’]¨)æåhÆßLfúZ±,ƒ£Q¿Ä Qô]Öª-¢Òc¡¥[&E K|ó-G)æ€A4Ь•;Q—ÜL>,.)ñ,CÍ?V¡…,mO߈–¡÷Cöîk–àM¨ÀÃäÐçS†~´EwZ/>Dζ\bîgŠéX¼5XÍŠÜ„ŠËfN2'ÖÌñ7¨»OL ¢½úKQ ªõ4¡™_/–¯ày¨Ó;ÉŽ\ü_$ü™¬ @*èðÐçcñð?¨Aé: wQc¾(‚·ÿż˜IÇ`”µ±¹ µ¿‹bÆosýÛW­Þ„‚o~ŽŠw¼8.ôùB ðz¦ý§(†#zbVÔJLEᨱp$ÚÞë }"y¢çŠ‹è¹â¢¤´÷'‘5ðß(~"FZ€7 â"Ge!‹0s¥°Û¯•z~?ñ8g¡Ð×뀃ý\ú|rEÿ#×ÐÿÈ5m‹WnF¹£Ä¤cˆo›Ôœ¬i[¼²'æç!«°Í1-a¢8ý5m‹WÆ|Ó³Jÿ#×жxeò¶_gþy1Ê`Œ‰V´MÝ ÜÖ¶xå@¬ÏC¦À2©V¡h²˜hEë€ÇÚ¯$Ö›žeŒ5@Û╻ߢ$©…È‹ÉHzMnî‹õyÈ¢`<ºá12­UcrPæã(ì~¼ øjö3QâÐy@”•…2eó:ð'Ä[ hZšt¶-^Ù£êçËØÜlBVجÐçf1å?Ül‹ÍÈ¢0¸vJiA]gþâTý¼a¬ƒ¨Eü;` ôyYœ |å;DE-€9À™(†|Ð|‡Ø„¬eºÝl‰Mõóˆ±@¥Ñ:ÑÑR´dŒEè9½±mñÊþXž¯N“ÎŽ¡Ä½¤–ÿÃ@ï¹®êýVà T…fù¬ãÍŸ‹P–`,5ë.GQl»Šà ˜çd**ïý1âIÛ‹ƾK$éß!`êÄòTÎë»(r°Z!°±ÂF'2\žªƒaa89 çšÿŸ„?aèAµäþ‰"4Ø+湇Ò?O<Žã'P+²µ>\Ø›˜Áߎ¼äE3ùŠòúJ—}  !(ÅÊ;Ÿˆ¬£) ‰Õ0 ÃDÒ†-À»0ûÖ¡ox3aù_ÎþmËÅÀU(´ykèçÁ‹X¦ÿÛQÏŒ’_éî¾ ü•År"¥XÂ0‰aaXÄHQè@–Äó{^§›Qû†Ð7¼Ù°DàEÀ—‘„¦e²~ è ùL¤.Öà? ™ü'Vøõ.ä8ûRÉ}Ž”b Câ?˜‹ÌÆäu<†#‘ƒib ×oµ¢þ(ÐUˆ€_,x*"ûZ í@áì?ƒp–¡/˜|xM•oÛLæ¯k€C>D ëÁ‡ýTàXF:GÎÈ™H8&Œòq{Q·™K¡X øÆº— %ðz‹ÀZäx"—`ÿx´îÿµo;îDUu.A©¶Uï¤IIÃŒÉ(6áy /#!˜—¦ ÿÇ:tÃ…ø&BD}>F ŠQ©}yËô=*’qT·¥~ Sl#!(ÅzÀÚÑ ŸŽD ±¢Ú—xí׬D(»÷¡-cï“BÚp¸/uð‘I¹­ Z€©sëÐ7ÖC7‰ÃŠmÁ`”ˆÀ× _Zþv”ݺ)`mù}>¸Úòš™â¹OÎ@êýTŠi²%@±ÙÀÿÃó Ä\à¯82$}rJ°9Øàç<ˆjü;JðòÉ\dYßÙ¾ju*Ù‚u›ìÖìÿns¢!Ù²;¡n§_ˆmŸ˜ 5f¶Ì Ö÷9¥Jû¦akÃp—n)ðzÔf,]³ÿ2QB2€¢ÿ~ ™óø·£g1윤ÁDT}'³ßψÀ\öX€S8õ˜hKÃP—X³ÿÛß…åZts²êô;åNä‘ùÈÈ¿C‰g½Ž}!*gæœF,€— ó$$›PÐÆ³Ï£ŽÎü,¬ïqò¯d+TøRàÆ§Ðâ:ï4U³X ?o'ìÚ¿m÷­Ì™þ6ã€óÑDž˜Œj@ÆÒ™ÇÛ3pçã¶ Gû1®?¸^ `)ð:Ï¡”k1V2<ø^Jþ: Ÿ€›JPQ`9¯Au*}³ x%¸µj3û· Ç_ÈuëfTW}gÀspÉ\à\Èþ2À:ÿs ¿;”‘ÓÙ÷²sðfT™ÚõXó <y›”/} dÚô·iCË€i¡OÄÓÌ÷ÉTóÙ±() rY€S8e :£j°2þ~Ÿ”<’U²ø ?'ƒ?áÅTnš’%^€œÄ¹ÃJþ6þ+ÍÂ45qµ ¨Õ˜œ¡;P³MŽŸ&sÈø2À:ïWOGÞ´¸øq€ãžCg`­ðRÂ:v®Æ8`r6ûƒ|+çã¹&\ ÌD5!BwÝI cô£mÁzBÏáD€çÄX•óÚ‹ õ€î¾†é˜S–&éÄ%KQ#ÎfàAà—ž9 m¯¶»ø°Z,€ù„­öóà&ÈåìŸ0 xdo`ïJÂG‡¦Ž±zPÁžŽêŽ)–óï\Ôâ*Ï¢<ÿ¬†ûÖ«Qâ,2 8/ôIxf-¦–¿GŽV@ãË€j-€ÉÈûÊù÷sL¦_°Œ³¿Rf9Ù_ÂÔÊA´%xÈã1Ç#¡m8ɪZX¼Ìã´i¦ÙäDËÔn€9ϼ81«ÆŠ ¸¸×óáOG–@CTËü%)Ä!WÉ•4ÏìŸð*²·6#\MÈvôœú¬)1³#×È2  `rL…ØÖÙüæ™ý^ˆ‡‚Ž9°bA°¬€«ñ<8›«zU#‹P”Zn@­“›!Ñ}`ί Eˆ:SÏã¿—ÀKipÇeT°ÌÿÓ£<1SåG(ú¯9—pË®Z™‹f£fæ ð+ü 9žˆŒeLBÅ*B$uÜ‹œ+yÞ÷¯ÄbÂ9^«Â²NN'éÌUc-nÄodà ôŒ%ÇÆüDN•íŽ “‘óµ-òeÀxòYФ6a²T=ò2¨V\V,óÿÅ„1C· sªYgÿ„³ˆp,&(¥€.ÔQÈç2`J$«‹Ö1þïLÂTt½x$Àqcãù˜Á›`Ï <¶³Žkp*æ‹ÙÀIõ¾¹’ÌNõøEzÐìï3²*V&¢e@¬uõ&"ïÿ„Ð'ñ4ã¬ÇPI¦@ÅFLåÕ&7ÿVà â+%æ#`Á0û€[=ódê´Ôký*p„ç/Êø Õ–9F¢ëdÇ9Ä+NÞ))¶×ã¡—"‹½fF³Úª¸èX ÝÀoÓ|!Vbí4yÿ}?#Yà!üFC~˜Ñ`:aŠ:<ƒÒ+ ó$1v:ì…+ûâ9à>Ç›FaØ£ À1Èà›µø/±”¢éTR÷o^Г‰—Cøu¶c VGàh° ÿi¨ËOáý?œØºMC‹6—Í–à!oM^ ©c™8âZÀE( Ø'»h¬½wÞ‰©{PnË~;äaü¶[HÉXå|°¤ÖrÀ„i¿œ†nB-š¬ìw£lÃoùúyÔQJ®œL#Ìþÿ=ÀîÇM›Ý(¸©QbI¹‰»²ßý@_àï“ûÇ=o*u8ŠË Àlü·tîî&ŸÃõ¸«iCÑ —e¿×¢ûžGºñkÑNžWë›Ê @þg™=¨ÓJ×ÿÛ€_#'g£Ì&Ðn@ e¿ûQMýg¼~XŽÀMø‹i ʪe' œ,¤ôÂ:Ù <éù˜¾®ÃM`H €¡ oº,ûý úÊ3›ð»p,5Öîkà8üoï¬Ã¯ÇÔ'­(³ÑU¹¨S Wz{¹ÃcߌÖÈyÞJ|¿ÛÚÇPãV`éÅŸ@ë<Š_¥ôÍ^Ü•‹òÞ=(…²ßÝæzì'Ç=ÑÖöNÇ›M™™¥0ÿɽHò¸þ=à-(Ãñ)GŸùJü'j¹,û½%}åyö ÝVÇ›Cƒ0ÿhßõB+°w-¤–§xþ.w ®GŸyžýAðœÇãM¡F ­TŽ@‰@>9€f„¼Ó‹v\,ufà©{P e¿˜ëÐOþ ¿K€ñÔ˜\*Gã?ít+~s§Cr+°ÞÑg‡¿rí.Ë~?„ÿúù¡èÇoçà Ô¸E[*³qÔw¼6Ñ< @[€k}ÖI˜’Ði‘RÙï«ñ[3/V,€Ï¾mÔ˜0Vn à[¶¢jªÍÀpn,_݃\–ýÞ \“æÉFÈNd ø á%À,ü7y(îÂ]®ø94PºJ\–ývùݳ‚φ¡­ÔX¡©FMóx² ‹óäv °.gÁŤT”3…²ßƒÈüoOÂ>ü帴R£_ȶڨ³°`øö’eýÆ É×â¦ëÑ”™—V÷ —e¿· hû:4ûð[㲦؊Rð]¤—ü†WâALíCœ…¼ôià²ì÷-4g½‡$,Jl¨Ùà€.Ùlìgx/¼Q¢NÎbR(ûí2¢À!¥æ‚o¥ÀM±ŒÌ`™¿7à¦ÿÁ$”!èº{˲ßÌ÷m6ó?S ú”àÖ’Ÿ}oöÑ|N¡„õ¨ª ~÷E\\–ýîÄoy¬˜ÄïNÀ jØÉ³ ÿ &}4¯YØ… b¸ˆX€£&¢)”ýNBûštöŸ†ßÞŽ©aii @ ùÏΊk ¬ÁMhðä­wÆí²ì÷:ü÷Ê‹‰iøížT“S½ðaÙŒ» A—݃\–ýþ Mú[ß~µAŠ\‘Ba¬€~dïwð‘ÇÒ`ÑÊ~ïFÁ?ƒMjþƒJuûŽ®­šB³Å4Êxà54³?we¿ï&¿U+byá}×׬ÉáØZòFŸÞʱw‚.º¹*û„þîvôݲHn*(WË 5^o[zk}³ÚpfšI,³øÜ„D×Ý=(…²ßÍúkÓNÝz`U!ª[úñŸ–"ú0FîE™rÒhå—e¿oG;ÍÌüm:¶ÕCûZð»G+{QÍC©Ý·7e¿{QÕßfñHð]c³f+Þ€Qy5—0Ê%‚[|dÍ݃R(ûýðÛ’ï׌_ ·aðY¾ á»A¬¬ÇMÀL½Ýƒ\–ý¾Åÿ7;ÇᦒRµ4$ýø·Æã?ü8V¢˜¹ãõtrUöÛå÷È:Çá7ž‹³k[aD5ž}Oä´š#ª5%‚êý‹ª»¥Pöû1”ûß´æ¿0¿>®íÔèÈ/u>‡ÿú|³ …¡µ³jéä²ì·+_FÖ™,ò|Ìç¨1½¾tàí¿é6ÿ½bÅ¥÷|9ctJ¡ì÷´›ÑÌ¡¿ Sç{>æj,±_Ψ)À¾×IQb WûçÕvrYöûw4ièoæà.9«Z¶Pã^*Ûð/ó(vl†"èð ÆDqYöûZš³Ä[9–âw pxªÏ„ò€oGàT´$c$1ô.Š¥žÄ(i½)”ýÞ†)wÞÌæ¿å\Š_Ëö uT]*€.Ô¨Ã'S€ã=3vîAæt£LGÎÀѺ¹,û}êûW åÔ2ÏǬ«Én©žö|âíoi3o–° ãLsðYç0zn¿«²ß}hïßw Y¬Ì¿ì@möj¢œ<åùÄN¤p#ÌçëpÓÙ‹,Æš¶¡¼lÃÿNÀ8T†ª Éd^7Jbî'qé.Ë~¡¿Œ0ÿOÇoP5©ºÊË—{öxþ Õœà¸1Óƒb\TjZÁp÷ We¿÷"(f@Ûªgã·Èüß]ÏË À!àQÏ_´°0Àq£ÄP7!3»Qæg˜Ÿ]•ý~¸Ã÷µ‰ûûä~êLå!ÖNÀ:ü‡‰™• ?ÀžÁMhðDT6ühLpƒÏ¼ í?75–ùn¶Uk¡4VëúF_>€›PÔZhCêÙ´U‚K1VÀ2³]„hŸ¼8ÙÁgm§ýµ™‚¢*}›ÿ;Qäh]Œ&› ³¯û2Üx¦óÆZ$ÊrðIÜ”ªvÕÐ$/,ÁMTe­¬Ã$ÕÃh°7±èµrþ÷P³ÀsÈØ(í(ñªÑVUýhïß÷vqtXæÿkÑòÊ7wÑ@?Ñ 9\”©®…IÈ‹ÚVøDIóçBŸa3ÐYr~ÍÌ1hiå›C¨~D]ë(#–#ð.ÂävŸƒGJx™Ý1°EŒ65Öì6nú)ÔÊ,ÀR)ä1è›ã3¡Ø (afÏ=ðyt¿Ä©X™ ¼™0ÉlwÓ`V%ØÜàKMDæTQ'ÐP’!X·ÃÇÕéP„þÚ¼wýjaE‰ÖT°”²`–ýh}"Écþ+ªfÇQ`PH®G>€¦Æ˜ÿíÀÛðÛ4a;¦üz½ë;ü6ê(2à€yhOµXŒ¤ í„ʼۇ‚ú‹ÙPååÎ?®áˆÝ±`3 4x^¯ÇRE´X.dî}LŽÈ`˜Ù¿ øüþHXƒƒà°±àzà|% …PÂPõ\M<[‘¡9xS coCá†Ì¨ Övà „¹é“·P” ÂX}h ú®¿÷æ¡kfóßZûÿ1áŠØÜ‰›ÞUåƒ?Ü苞‡›´Õ¼q+Ž€¸7áÈ™ÅÚ÷? ¸ Ðiô#?Œ“òýÕÀP»§_ö(à@{± Á è‰duøî#“?åðk¾ØŒY6jþÃ`-:qS¡¶^K˜(«(±š‡\…¿HÍ¡”äf5ÿ­ÙÿÕ„óüƒ|r.êCÕ—„Z‡› µõp,ðvŠü€R~‡¿WEI²ÎÑÀ_ –!8\ÃZÕ €Ë µõðfÜä°ç‰=È,O{iÖî}Ó†þšÙ¿x'&L=÷aÁ\˜ÿP…Xˀ멣÷˜# ¯ká à°Ðà´ r>)LÚ¬æ¿áTàƒ VõÍ ðsäÿqF-UaŸÄ]×Úz¸€¢dX)ë0é )rEè/(ðg= ÆÞ7À&àJp7ûC•`¬€^àgøïœ0øsTz©@DÎÀ´–f(B֢ȿ÷¿Á¿üŠªu×Zþ6ƒŠ×¡6WMoX²“ô–fÎù =W\”̺;ÿÞŠ_!ØüÐërö‡Ú`ðcÂ9§"Lé¥XÙ€Ìô4¸ÇkÎ,P~CJK½ªÀrþ  Å™À{€Öf· ½ÈKﺟãNŠª¿eñ,û€KSø\ ¾ÖP‘72ãÑŽÀò€çÖÀ¼•:[CUà.àÞÐß1f< Á ˜ú‹®Í¨±†ù¹¦³cÉð#¢{l-ïwÈ À_ÒÙ±d¯e4+[Phð2GŸ7€œMú»àâóæ 4ßû¸ªâ{’Ù¾ju"W¡^ŒïC½.êMhëþ›ïõ6‡|Õ… É…ÀAs;­æ!Wá®™K"(Meþ›Á?ø"ð}LÐù÷1IÁ"ø-º¯©ÌþP‡˜Ù¶)SȶP¿NæÃݸËÖKcI5ÖàÿGdÝž |•§«ZÀ™ì¾NÊù´‡¾7Í*áDào#ŸG ìÀÍýHË©-%ƒÿ] 7NYF" Áõ8ÌúFàðmóåBòälÚ]’æ!fmnÀl+6ƒù_að'4$P—ì.ÆCÐ]]`9ÝÖ`Ö(i>¼ š~)ð ×ìK3°È'»/¸øüd0Ïþ•òƒ?¡a€š„àrRôüÛ4ÚÉ´ Y+Y©žieæŸAù õTJ=€Òkgâ6»® ¿<÷?D^ìZiA™—}žfÿ~Sr·ùÙ­H ˬYxðeT€v¬vé‰üpÓ‚‹Ïg¬ÝѨ°kð~´³v Еöà§Š/]3Û¶_Afxh~„nÐŽj·.è át[F¦‰fÚƒÊ|— ÀTê»·Èììõt®-(º³=…Cô¡mÌû»Ô9ømîGÖu‹€Ulä(d‘<ôD/0$/.#|O¿^´–û,Ð]ÄØ8ü w Ùú>p#¡p5ãÝÂCÔ ´|˜"T¸ ‡ƒTôß1Wõúb a°Úˆ}e…f:ð)LݶB þ„È'ip¹æ}9/zC)äü"Æk[ˆ@ób Ìp7ø2/NÀZkÿ7饦ÖÊ Ðö΋ fÄËÐnÕp7ø2-®½ÞÛÑ  ”p*Ú¡X…4%ƒÿ«˜6s)‘Yp&–p-ðƒÐ_ÌbÄÍ)D ÿ”ü+<6yÎN*9‡¨qjXµ¿F¸vbå(D I4øV_Ž/9—hI#ðäüü7°¬D!9'ðàO8-ƒ”œS”8k)ðc‰,‡BòD$ƒäd|=Úqˆ^R±ŒþÅdÇÄ ³p"¬vð  þ„̈@ZK€„Ï_i©åÀ«("35°V ÁFès2dBRk)ðໄ.ez`>L.D {” þ!ÿNDD/®ƒ"à ¬à?ß ý…˰=<ÿ <#ī鰄ð¥(5õ.`+¦#m,×&ƒßfµõú0ªªMQÚK€„ (>?ÆsS¿BÖÀ ¡ð Æ_@Å*¾‰)ÅÞÙ±dzgÇ’–H®Ñ±æcü ‰ö Àj"kj“ºCªõöûõ—IN›{€ObZ¡Ç2ÛùÆÜ¯6”Z$¥÷! é^Ô.ìf”¿ƒ×ÊXѬúw¨®@ì  jÃlÁ ð"0ôPMþ SÃ/ô—…$œùbLÕãfs¿–  A/(ùïAT8ä TAøT$öiL1Q×Ì!˜úºUA?xz¨ßB±ÒÊ4}Õ:l6°¬¶¢rkm~½õ¸¸ ‰Â£¨¸å@š×®Æ! ‚ ßC3LÌ<‰<¸ßEsSYæ~Í~ ¼¸Ê·õ£Zö ²Y7£z‹[Q? ç×°d9ð÷d£…|"àU`„\ˆDå)C²¾ˆ*îö7‹X÷êHk­Ý7ˆ–›‘ßàd<‰ÅœYVF&Ÿ@ c&½xÕáE̵™€¬ï @Or,ï#ÌË¢@¡©!ΣFžBÞðï`v3šA̽šƒj=¼¢ÁK‰÷ 1Xƒ `îÂ#Ñ<èÓÑRàCÈ*ˆTEÀ\“iÈ:šv¼z°&¡›õפSÖ5}À¨Ü•˜Ç< e¼Õp%Öƒèú­GÖÁ(æàiLüz®k†Eà{hÇe· °â$æ›kñtÿþ è.0ôpÍD;ï#ÞR º_E³Ø¡&i(˜kUJ‡éBŽÄ»‘ßààqêp$fTzþØ×ˆXƒÿ4daŸ‹ÆÖ§Q<Q =\G£Áô¦ÐçS#;_ -ûÈi)rË X‰–i÷bL‰ë܌ⶽÕ\㌊@ª`õY`o="`¾÷xxô†íhðUûscƒâëÀï‡>§Dæ¨4úäÐ"0÷i"*öò^‡D×S ;oGÎ¬ŠŽÄfk½ÿA´¬>ÊúïCÀ»Ë¢!K‘„N笇A8t 2•oö@~|æ>Žj=Ì t½À6dÜŠâBÖØa;4Ö ø{4fÁ×T“˜ïØ‚*}x'‡GÛî.~À]ÐÁ{ÐluŠ¥ßLÊ1>0÷(ÉH¿oÕØ ¢k½žá]…µÈZÚU0d&ÚÊ}•ƒšb¡*°ÿkÑ9å}iÏ"_Àº(r' À—uÀ/_¡Ykd×*0÷è$”æ}bèó)¡å'¼ Xo_ã*Ú€ÇHE°Öû që¨ðYç[ìωÊënݰû1]XCŸSƒ´'~†:÷~8©³cÉäÎŽ%YÍ<|í]»ìèë‚ (ðhW阇~;2‘c<÷r$!ΦصÌÏI&ëר<øA˦®ÒŒÊHÈ¡%`Ó‡œ†këÙú$² 2‘s`åt\œú|Jø<ð·PÞÊʨ%pyô¿„©Ë€R¡?Ž{“«øŒ+€·cA¤#Dà…ȼyMÌç['vŠmâÝ~Ðü[ÝÁ0iã D8-ö#óÿ—•®[FE`/Z |mí}xÕ[ñ— «º/#´$o$7«Ñ¼õT¸-…AæÛLßEߢ`݇ (Ñf.òœƒ"Ìb©ïð0ðj`óX×(£"°›áĬÕøÞÏ¢< 2#0âᛃ¢¤ÞC6=%I¤I¼Ü¡åÂFà$ ]ÈÙØõ‹ƒuÇ¡ë;mŸ‡ÌýPÅEHgß¾ú·?¡¶`¡Ùhry‘ùÄ2|ør©#1zH0è ´ýô²QÆ5ƒhÀD>ƒg?a›yíB¦âN${‘H”»Ï3ÐÚqŠì› ÌB‘™Ç ™~ªõ{1Ó‹ÿ·¡z!4"ÕHÔjéBÁ[?,€q¡Ï¬ZÎÝð0Kö µÏØ17ôyy¦™Û“Ð@í(ùÿóê1ö™W¹Ïg^­h ßJ†&„2lE~”z¬ ­hbÛöá±Ðm¾ãadÊä17öнÿc”JZ0L+Ô“ÑÌ=…ƒ–¾f¡ “6²ÿÐß‹€jš7¢HÁ+‰¯„}£t‘€!@7ê]ÀÕæïÍÍäD­™&ý˜ŠV¥dN`„‰w íüq5"-ðË. \·4ç"P62* mnö3(Òî¯1M šŽÇpЃ2Ç"°á¢dV¬F¤— %Áõd#̳À·`J¸7JNEàYò*0Â/p#J…ü£¬y rÇ!´þwF]" t†þ’ ²ò»Aù8lIð àýÈGuõ.¨ÌÓ¨“S,xÅd51m-Êö#Ì$èBéªoC[†±µ'/pÇ](.Ä9Ö€Érvj7²Ê’;€ÖÀ£(]òý¨ÃO±]˜/úѲ¯«Ñˆ@7¦ëu9r) VàÐeÀ[Q ð­|fATì@%ÅSMʸ4Ÿ`cYoàmȳ{0ô¹4Ì}(Q*u2,©°C’{H0"ÐÜ€v þÕ /¶ ³ËPáUdT¶Sa‰Ô4#¬=¨áç…À'QŠb· [ìEÛ^ë#”ˆÀ‡Ð$3£Æ@“ @‚‚A`Ê.|#ŠØúÜ ªf#Jòàa¸ø $±²BÊc„`ÝÀ¡þw— Õ,,‚¸¹• Þí´±Dàf´ˆU¶c*I•£© ÁA/ò(Y_GB!ñÑ^_£Ô–Ü„ºñ±HÆ`[ɹޠ #Ýhvù꯶Å}Ø F°P ^4ÕX· ŽI19•G-’™Š@>1UogÇ’{Q–Ù7‘¼•*¥f³r7 Ž))6•Üú qí¢B P#}KE–ÀP%Ü7g£ê:Y¯¤“En!pý«IÇѨùè{‘ÄDÅ ( *Œ vv,yu÷ù9²^‡º/&¾›ŸWv õ0óßü'ŸCÏ@ŒËéŠA@P@MXÜÁÎŽ%·£ü‚¯/GKÎF%´ci”‘GC>™ ˜ÁߊýçÄJÅ ( n¬[:;–\üxZ"œ¼ÕÔ­v~ÖYƒÊž{§d½ÿwÈü™­Tˆ€Bœ`9 Ÿ@ùãÿ…êê¿õ5<õnŸNóú úQí¾uhŸ‚ºøÔÂdþ{í¡˜‘õ~9v ð÷Q)À!ÖCÙ<ÙÙ±äI”‰8 µÒ:8Íü<•ïΫ  æ%IÁ޵æõ8šÁÏ@æs-½žÆsèm†ÖûåØJ!áHv€­K¶¢z…“Ñà? 8Õü¹Y SPsŠ,ÒâóŸB[§÷¢Áº øî䚘.Ow qüP ǸéÜ[ï—£bàd'™±‰up%Š)˜ <8u~]dþ> Æ$â™uúP…¨üÚcæõ°ymFB04àË]‹ÎŽ%½À·óta•ǽ™1Ö´®Èàz¿”1ƒ€ €`XãyméìXrè“Pïù¨ý×|ä`œ‡,…ch´›×xt/ BDf{¯yõ ¥ÌTrk ð›P"ÎFäeÞo~¯žm¹ûÿDmÞÆ¸çÐ"Õí?ËäŸ|5£œÚÓ£S °…D„µ³pÀ¼ž~gLæV4Ð'¡F3P Ò‘H ¦¡&ŸíÈÙ˜8©Í{g¡½Çü[‹ù3i št"Þ‹**ï@PÒ¸ ‡íÉ0€‚«.dìv×÷"«ÚQ>Èiæ:0×|‚¹±Xc•3 È–0t3lo¥Ì~¸%¶s±=ÔÉ o3€g¯z ëÑR`5•ýkðWܵø> úš„„vZ’m^Éßç˜×D†ÛªÇ ‡¨"[2¯肌`k.ªâ|Æ(¿¶x3p]èä³DhAƒ<± &"K,ƒR˜Ë°?'yOÒ‘9-îDÎËç '`Aì<‹Ò¯O¡|¢ÕzTÿ/8f0 "IÚ꫘ŒåG(‰YHl‹Âˆ9¨«sbM4" $Pk[ðJà7(¿¢”ÛqÔúËÖŒ›8T“®ÅCŒ–HŒc¤H̤ü2c¶õó FZ圿£ö´)  ö &.+Оÿèmþ»Ä‰>óJ²Ÿ‚ÃD¢áA?“òËŒäïÇ¢€«ŠA@Pø "ÁX“PI¶wXÿõ$ ~÷mýú½ÞfŸf˜Ua‘E6%Bª(ÄX‚IФø¡–!©’2U E‚RI‘RÐ(È `á ˈìË ‹Â¬ =Óݯ»ßzsîë™a`“[}gnß¾ïÞs¿óïœGêêê`ÕªUL&¡k#’5«¶~€X€ ñåö'ð0¢fƒ¨d±¥`%ïR­å6^ ±æ›`¥@ „`ËÖm‘‚¾šÇaß¾}`YÖƒ¿H¡0Œ{á¨<4À± C›wÒs5Žq ½[@Š öÛúÛ7M8Ê΂ýc ¾žgµœ]øb·‹ D #include #include #include namespace MoleQueue { /// Convert an IdType to a string. This will prevent the literal value of /// InvalidId from being used for serialization, RPC, etc. inline QString idTypeToString(IdType id) { return (id != InvalidId) ? QString::number(id) : QString("Invalid"); } /// Convert a string to an IdType. This will prevent the literal value of /// InvalidId from being used for serialization, RPC, etc. inline IdType toIdType(const QString &str) { bool ok = false; // qlonglong = qint64 IdType result = static_cast(str.toLongLong(&ok)); return ok ? result : InvalidId; } /// @overload inline IdType toIdType(const char *str) { return toIdType(QString(str)); } /// @overload inline IdType toIdType(const QByteArray &str) { return toIdType(QString(str.constData())); } /// Convert a QJsonValue to an IdType. This will prevent the literal value of /// InvalidId from being used for serialization, RPC, etc. inline IdType toIdType(const QJsonValue &json) { // QJsonValue cannot handle integer types, only double, so round off the // value as a double. return json.isDouble() ? static_cast(json.toDouble() + 0.5) : InvalidId; } /// Convert an IdType to a QJsonValue. This will prevent the literal value of /// InvalidId from being used for serialization, RPC, etc. inline QJsonValue idTypeToJson(IdType id) { return id != InvalidId ? QJsonValue(static_cast(id)) : QJsonValue(QJsonValue::Null); } /// Convert a QVariant to an IdType. This will prevent the literal value of /// InvalidId from being used for serialization, RPC, etc. inline IdType toIdType(const QVariant &variant) { return variant.canConvert() ? variant.value() : InvalidId; } /// Convert an IdType to a QVariant. This will prevent the literal value of /// InvalidId from being used for serialization, RPC, etc. inline QVariant idTypeToVariant(IdType id) { return id != InvalidId ? QVariant(id) : QVariant(); } } // end namespace MoleQueue #endif // IDTYPEUTILS_H molequeue-0.9.0/molequeue/app/importprogramdialog.cpp000066400000000000000000000070121323436134600230750ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "importprogramdialog.h" #include "ui_importprogramdialog.h" #include "program.h" #include "queue.h" #include #include #include #include namespace MoleQueue { ImportProgramDialog::ImportProgramDialog(Queue *queue, QWidget *parentObject) : QDialog(parentObject), ui(new Ui::ImportProgramDialog), m_queue(queue) { ui->setupUi(this); connect(ui->fileButton, SIGNAL(clicked()), this, SLOT(showImportFileDialog())); connect(ui->fileEdit, SIGNAL(textChanged(QString)), this, SLOT(importFileTextChanged(QString))); } ImportProgramDialog::~ImportProgramDialog() { delete ui; } void ImportProgramDialog::accept() { const QString name = ui->nameEdit->text(); if (name.isEmpty()) { QMessageBox::critical(this, tr("Missing name"), tr("Please enter a name for the program before " "continuing."), QMessageBox::Ok); return; } Program *program = new Program(m_queue); program->setName(name); if (!program->importSettings(ui->fileEdit->text())) { QMessageBox::critical(this, tr("Import failed."), tr("Failed to import file '%1'. Bad format."), QMessageBox::Ok); return; } if (m_queue->addProgram(program, false)) { QDialog::accept(); return; } // Program could not be added. Inform user: QMessageBox::critical(this, tr("Cannot add program"), tr("Cannot add program with name '%1', as an " "existing program already has this name. Please " "rename it and try again.").arg(name)); program->deleteLater(); } void ImportProgramDialog::showImportFileDialog() { // Get initial dir: QSettings settings; QString initialDir = settings.value("import/program/lastImportFile", ui->fileEdit->text()).toString(); if (initialDir.isEmpty()) initialDir = QDir::homePath(); initialDir = QFileInfo(initialDir).dir().absolutePath() + QString("/%1-%2.mqp").arg(m_queue->name(), ui->nameEdit->text()); // Get filename QString importFileName = QFileDialog::getOpenFileName(this, tr("Select file to import"), initialDir, tr("MoleQueue Program Export Format (*.mqp)" ";;All files (*)")); // User cancel: if (importFileName.isNull()) return; // Set location for next time settings.setValue("import/program/lastImportFile", importFileName); ui->fileEdit->setText(importFileName); } void ImportProgramDialog::importFileTextChanged(const QString &text) { QPalette pal; pal.setColor(QPalette::Text, QFile::exists(text) ? Qt::darkGreen : Qt::red), ui->fileEdit->setPalette(pal); } } // namespace MoleQueue molequeue-0.9.0/molequeue/app/importprogramdialog.h000066400000000000000000000024451323436134600225470ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_IMPORTPROGRAMDIALOG_H #define MOLEQUEUE_IMPORTPROGRAMDIALOG_H #include namespace Ui { class ImportProgramDialog; } namespace MoleQueue { class Queue; /// @brief Dialog for importing a program configuration from a file. class ImportProgramDialog : public QDialog { Q_OBJECT public: explicit ImportProgramDialog(Queue *queue, QWidget *parentObject = 0); ~ImportProgramDialog(); public slots: void accept(); private slots: void showImportFileDialog(); void importFileTextChanged(const QString &text); private: Ui::ImportProgramDialog *ui; Queue *m_queue; }; } // namespace MoleQueue #endif // MOLEQUEUE_IMPORTPROGRAMDIALOG_H molequeue-0.9.0/molequeue/app/importqueuedialog.cpp000066400000000000000000000111671323436134600225600ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "importqueuedialog.h" #include "ui_importqueuedialog.h" #include "queue.h" #include "queuemanager.h" #include #include #include #include #include namespace MoleQueue { ImportQueueDialog::ImportQueueDialog(QueueManager *queueManager, QWidget *parentObject) : QDialog(parentObject), ui(new Ui::ImportQueueDialog), m_queueManager(queueManager) { ui->setupUi(this); connect(ui->fileButton, SIGNAL(clicked()), this, SLOT(showImportFileDialog())); connect(ui->fileEdit, SIGNAL(textChanged(QString)), this, SLOT(importFileTextChanged(QString))); // Restrict queue names to alphanumeric strings with internal whitespace // (the input is trimmed() in accept()). ui->nameEdit->setValidator(new QRegExpValidator( QRegExp(VALID_NAME_REG_EXP))); } ImportQueueDialog::~ImportQueueDialog() { delete ui; } void ImportQueueDialog::accept() { const QString name = ui->nameEdit->text().trimmed(); if (name.isEmpty()) { QMessageBox::critical(this, tr("Missing name"), tr("Please enter a name for the queue before " "continuing."), QMessageBox::Ok); return; } QString type = Queue::queueTypeFromFile(ui->fileEdit->text()); if (type.isEmpty()) { QMessageBox::critical(this, tr("Cannot import queue!"), tr("Cannot import queue from file '%1': Cannot detect" " queue type!") .arg(ui->fileEdit->text()), QMessageBox::Ok); return; } if (!QueueManager::availableQueues().contains(type)) { QMessageBox::critical(this, tr("Cannot import queue!"), tr("Cannot import queue from file '%1': Queue type " "not recognized (%2).") .arg(ui->fileEdit->text()).arg(type), QMessageBox::Ok); return; } Queue *queue = m_queueManager->addQueue(name, type); if (queue) { if (queue->importSettings(ui->fileEdit->text(), ui->importPrograms->isChecked())) { QDialog::accept(); return; } else { QMessageBox::critical(this, tr("Cannot add queue"), tr("Error importing queue from file '%1'. Check the" " log for details.").arg(ui->fileEdit->text()), QMessageBox::Ok); return; } } // Queue could not be added. Inform user: QMessageBox::critical(this, tr("Cannot add queue"), tr("Cannot add queue with queue name '%1', as an " "existing queue already has this name. Please rename" " it and try again.").arg(name)); } void ImportQueueDialog::showImportFileDialog() { // Get initial dir: QSettings settings; QString initialDir = settings.value("import/queue/lastImportFile", ui->fileEdit->text()).toString(); if (initialDir.isEmpty()) initialDir = QDir::homePath(); initialDir = QFileInfo(initialDir).dir().absolutePath() + QString("/%1.mqq").arg(ui->nameEdit->text()); // Get filename QString importFileName = QFileDialog::getOpenFileName(this, tr("Select file to import"), initialDir, tr("MoleQueue Queue Export Format (*.mqq);;" "All files (*)")); // User cancel: if (importFileName.isNull()) return; // Set location for next time settings.setValue("import/queue/lastImportFile", importFileName); ui->fileEdit->setText(importFileName); } void ImportQueueDialog::importFileTextChanged(const QString &text) { QPalette pal; pal.setColor(QPalette::Text, QFile::exists(text) ? Qt::darkGreen : Qt::red), ui->fileEdit->setPalette(pal); } } // namespace MoleQueue molequeue-0.9.0/molequeue/app/importqueuedialog.h000066400000000000000000000024741323436134600222260ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_IMPORTQUEUEDIALOG_H #define MOLEQUEUE_IMPORTQUEUEDIALOG_H #include namespace Ui { class ImportQueueDialog; } namespace MoleQueue { class QueueManager; /// @brief Dialog for importing a queue from a file. class ImportQueueDialog : public QDialog { Q_OBJECT public: ImportQueueDialog(QueueManager *queueManager, QWidget *parentObject = 0); ~ImportQueueDialog(); public slots: void accept(); private slots: void showImportFileDialog(); void importFileTextChanged(const QString &text); private: Ui::ImportQueueDialog *ui; QueueManager *m_queueManager; }; } // namespace MoleQueue #endif // MOLEQUEUE_IMPORTQUEUEDIALOG_H molequeue-0.9.0/molequeue/app/job.cpp000066400000000000000000000156701323436134600175760ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "idtypeutils.h" #include "job.h" #include "jobdata.h" #include "jobmanager.h" namespace MoleQueue { Job::Job(JobData *jobdata) : JobReferenceBase(jobdata) { } Job::Job(JobManager *jobManager, IdType mQId) : JobReferenceBase(jobManager, mQId) { } Job::Job(const JobReferenceBase &other) : JobReferenceBase(other) { } Job::~Job() { } void Job::setFromJson(const QJsonObject &state) { if (warnIfInvalid()) m_jobData->setFromJson(state); } QJsonObject Job::toJsonObject() const { if (warnIfInvalid()) return m_jobData->toJsonObject(); return QJsonObject(); } void Job::setQueue(const QString &newQueue) { if (warnIfInvalid()) m_jobData->setQueue(newQueue); } QString Job::queue() const { if (warnIfInvalid()) return m_jobData->queue(); return QString(); } void Job::setProgram(const QString &newProgram) { if (warnIfInvalid()) m_jobData->setProgram(newProgram); } QString Job::program() const { if (warnIfInvalid()) return m_jobData->program(); return QString(); } void Job::setJobState(JobState state) { if (warnIfInvalid()) m_jobData->jobManager()->setJobState(moleQueueId(), state); } JobState Job::jobState() const { if (warnIfInvalid()) return m_jobData->jobState(); return MoleQueue::Unknown; } void Job::setDescription(const QString &newDesc) { if (warnIfInvalid()) m_jobData->setDescription(newDesc); } QString Job::description() const { if (warnIfInvalid()) return m_jobData->description(); return QString(); } void Job::setInputFile(const FileSpecification &spec) { if (warnIfInvalid()) m_jobData->setInputFile(spec); } FileSpecification Job::inputFile() const { if (warnIfInvalid()) return m_jobData->inputFile(); return FileSpecification(); } void Job::setAdditionalInputFiles(const QList &files) { if (warnIfInvalid()) m_jobData->setAdditionalInputFiles(files); } QList Job::additionalInputFiles() const { if (warnIfInvalid()) return m_jobData->additionalInputFiles(); return QList(); } void Job::addInputFile(const FileSpecification &spec) { if (warnIfInvalid()) { m_jobData->additionalInputFilesRef().append(spec); m_jobData->modified(); } } void Job::setOutputDirectory(const QString &path) { if (warnIfInvalid()) m_jobData->setOutputDirectory(path); } QString Job::outputDirectory() const { if (warnIfInvalid()) return m_jobData->outputDirectory(); return QString(); } void Job::setLocalWorkingDirectory(const QString &path) { if (warnIfInvalid()) m_jobData->setLocalWorkingDirectory(path); } QString Job::localWorkingDirectory() const { if (warnIfInvalid()) return m_jobData->localWorkingDirectory(); return QString(); } void Job::setCleanRemoteFiles(bool clean) { if (warnIfInvalid()) m_jobData->setCleanRemoteFiles(clean); } bool Job::cleanRemoteFiles() const { if (warnIfInvalid()) return m_jobData->cleanRemoteFiles(); return false; } void Job::setRetrieveOutput(bool b) { if (warnIfInvalid()) m_jobData->setRetrieveOutput(b); } bool Job::retrieveOutput() const { if (warnIfInvalid()) return m_jobData->retrieveOutput(); return false; } void Job::setCleanLocalWorkingDirectory(bool b) { if (warnIfInvalid()) m_jobData->setCleanLocalWorkingDirectory(b); } bool Job::cleanLocalWorkingDirectory() const { if (warnIfInvalid()) return m_jobData->cleanLocalWorkingDirectory(); return false; } void Job::setHideFromGui(bool b) { if (warnIfInvalid()) m_jobData->setHideFromGui(b); } bool Job::hideFromGui() const { if (warnIfInvalid()) return m_jobData->hideFromGui(); return false; } void Job::setPopupOnStateChange(bool b) { if (warnIfInvalid()) m_jobData->setPopupOnStateChange(b); } bool Job::popupOnStateChange() const { if (warnIfInvalid()) return m_jobData->popupOnStateChange(); return false; } void Job::setNumberOfCores(int num) { if (warnIfInvalid()) m_jobData->setNumberOfCores(num); } int Job::numberOfCores() const { if (warnIfInvalid()) return m_jobData->numberOfCores(); return -1; } void Job::setMaxWallTime(int minutes) { if (warnIfInvalid()) m_jobData->setMaxWallTime(minutes); } int Job::maxWallTime() const { if (warnIfInvalid()) return m_jobData->maxWallTime(); return -1; } void Job::setMoleQueueId(IdType id) { if (warnIfInvalid()) { m_jobData->setMoleQueueId(id); m_jobData->jobManager()->moleQueueIdChanged(*this); } } IdType Job::moleQueueId() const { if (warnIfInvalid()) return m_jobData->moleQueueId(); return InvalidId; } void Job::setQueueId(IdType id) { if (warnIfInvalid()) m_jobData->jobManager()->setJobQueueId(m_jobData->moleQueueId(), id); } IdType Job::queueId() const { if (warnIfInvalid()) return m_jobData->queueId(); return InvalidId; } void Job::setKeywords(const QHash &keyrep) { if (warnIfInvalid()) m_jobData->setKeywords(keyrep); } QHash Job::keywords() const { if (warnIfInvalid()) return m_jobData->keywords(); return QHash(); } void Job::setKeywordReplacement(const QString &keyword, const QString &replacement) { if (warnIfInvalid()) { m_jobData->keywordsRef().insert(keyword, replacement); m_jobData->modified(); } } bool Job::hasKeywordReplacement(const QString &keyword) const { if (warnIfInvalid()) return m_jobData->keywords().contains(keyword); return false; } QString Job::lookupKeywordReplacement(const QString &keyword) const { if (warnIfInvalid()) return m_jobData->keywords().value(keyword); return QString(); } void Job::replaceKeywords(QString &launchScript) const { if (!warnIfInvalid()) return; const QHash &keywordHash = m_jobData->keywordsRef(); foreach (const QString &key, keywordHash.keys()) { QString keyword = QString("$$%1$$").arg(key); launchScript.replace(keyword, keywordHash.value(key)); } FileSpecification inputFileSpec(inputFile()); if (inputFileSpec.isValid()) { launchScript.replace("$$inputFileName$$", inputFileSpec.filename()); launchScript.replace("$$inputFileBaseName$$", inputFileSpec.fileBaseName()); } launchScript.replace("$$moleQueueId$$", idTypeToString(moleQueueId())); launchScript.replace("$$numberOfCores$$", QString::number(numberOfCores())); } } // end namespace MoleQueue molequeue-0.9.0/molequeue/app/job.h000066400000000000000000000220351323436134600172340ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_JOB_H #define MOLEQUEUE_JOB_H #include "jobreferencebase.h" #include "filespecification.h" #include #include #include namespace MoleQueue { class JobManager; /** * @class Job job.h * @brief Server-side interface to JobData properties. * @author David C. Lonie * * The Job class provides a lightweight interface to a specific instance of * JobData. Since JobData contains dynamic information that changes during its * lifetime, the Job interface forwards requests to a JobData instance, which * ensures that all references to job information are synced throughout the * application. * * The Job class also ensures that JobData is modified in a consistent way. For * example, calling the Job::setQueueId method will cause JobManager to emit the * JobManager::jobQueueIdChanged signal, so that listeners (e.g. GUI elements) * can be made aware of the change. * * The JobReferenceBase class holds and validates a pointer to a JobData * instance and will detect when the associated JobData object is removed from * the JobManager (such as when the user deletes a job from the job table). To * check the validity of the JobData pointer, use Job::isValid(), defined in * JobReferenceBase. * * To serialize a collection of valid Job objects as references (e.g., to * maintain state in a Queue implementation between sessions), store the * identifier returned from Job::moleQueueId() in the data store, and then * deserialize the Job using JobManager::lookupJobByMoleQueueId. * * Full serialization of all Job details can be performed by saving and * restoring the JobData state via the Job::hash() and Job::setFromHash() * methods. However, this should not need to be performed outside of the * JobManager's serialization methods. */ class Job : public JobReferenceBase { public: /// Construct a new Job object with the specified JobData Job(JobData *jobdata = NULL); /// Construct a new Job object for the job with the MoleQueueId /// in the indicated JobManager Job(JobManager *jobManager, IdType mQId); /// Construct a new Job object with the same JobData as @a other. Job(const JobReferenceBase &other); ~Job(); /// @return The JobData's internal state as a QJsonObject QJsonObject toJsonObject() const; /// Update the JobData's internal state from a QJsonObject void setFromJson(const QJsonObject &state); /// @param newQueue name of the queue. void setQueue(const QString &newQueue); /// @return Name of queue to use. QString queue() const; /// @param newProgram Name of the program. void setProgram(const QString &newProgram); /// @return Name of program to run. QString program() const; /// Set the current JobState for the job. Calling this function with a /// different JobState will cause the JobManager::jobStateChanged signal /// to be emitted. /// @param state Status of job void setJobState(JobState state); /// @return Status of job JobState jobState() const; /// @param newDesc Description of job void setDescription(const QString &newDesc); /// @return newDesc Description of job QString description() const; /// @param filespec FileSpecification describing the main input file (called /// by the executable) void setInputFile(const FileSpecification &spec); /// @return FileSpecification describing the main input file (called by the /// executable) FileSpecification inputFile() const; /// @param files FileSpecification objects describing additional input files /// to be placed in the working directory of the job prior to execution. void setAdditionalInputFiles(const QList & files); /// @return FileSpecification objects describing additional input files to be /// placed in the working directory of the job prior to execution. QList additionalInputFiles() const; /// @a param spec FileSpecification describing an input file to append to the /// additional input file list. void addInputFile(const FileSpecification &spec); /** * Set the output directory for the job. * If empty, the Server will set it to the temporary working directory once * the job is accepted. Otherwise, the output files will be copied to the * specified location when the job completes. */ void setOutputDirectory(const QString &path); /// @return String containing a location to copy the output files to after /// the job completes. Ignored if empty. QString outputDirectory() const; /** * @param path Temporary working directory where files are stored during job * execution * @warning This is set internally by MoleQueue, do not modify. */ void setLocalWorkingDirectory(const QString &path); /// @return Temporary working directory where files are stored during job /// execution. QString localWorkingDirectory() const; /// @param clean If true, delete any working files on the remote server. /// Default: false. void setCleanRemoteFiles(bool clean); /// @return If true, delete any working files on the remote server. /// Default: false. bool cleanRemoteFiles() const; /// @param b If true, copies files back from remote server. Default: true void setRetrieveOutput(bool b); /// @return If true, copies files back from remote server. Default: true bool retrieveOutput() const; /// @param b If true, the local working files are removed after job is /// complete. Should be used with setOutputDirectory. Default: false void setCleanLocalWorkingDirectory(bool b); /// @return If true, the local working files are removed after job is /// complete. Should be used with setOutputDirectory. Default: false bool cleanLocalWorkingDirectory() const; /// @param b If true, the job will not appear in the MoleQueue user interface /// by default. Useful for automated batch jobs. void setHideFromGui(bool b); /// @return If true, the job will not appear in the queue. Default: false bool hideFromGui() const; /// @param b If true, changes in the job state will trigger a popup /// notification from the MoleQueue system tray icon. Default: false void setPopupOnStateChange(bool b); /// @return If true, changes in the job state will trigger a popup /// notification from the MoleQueue system tray icon. Default: false bool popupOnStateChange() const; /// @param num The total number of processor cores to use (if applicable). /// Default: 1 void setNumberOfCores(int num); /// @return The total number of processor cores to use (if applicable). /// Default: 1 int numberOfCores() const; /// @param minutes The maximum walltime for this job in minutes. Setting this /// to a value <= 0 will use the queue-specific default max walltime. Only /// available for remote queues. Default is -1. void setMaxWallTime(int minutes); /// @return The maximum walltime for this job in minutes. Setting this to a /// value <= 0 will use the queue-specific default max walltime. Only /// available for remote queues. Default is -1. int maxWallTime() const; /// @param id The new MoleQueue id for this job. /// @warning Do not call this function except in Server or Client as a /// response to the JobManager::jobAboutToBeAdded signal. void setMoleQueueId(IdType id); /// @return Internal MoleQueue identifier IdType moleQueueId() const; /// Set the job's queue id. Calling this function will cause the // JobManager::jobQueueIdChanged signal to be emitted. /// @param id Queue Job ID. void setQueueId(IdType id); /// @return Queue Job ID IdType queueId() const; /// @param keyrep The keyword replacement hash for this job. void setKeywords(const QHash &keyrep); /// @return The keyword replacement hash for this job. QHash keywords() const; /// Add a keyword / replacement pair for this job. void setKeywordReplacement(const QString &keyword, const QString &replacement); /// @return True if the @a keyword has a replacement. bool hasKeywordReplacement(const QString &keyword) const; /// @return The replacement string for the @a keyword. QString lookupKeywordReplacement(const QString &keyword) const; /// Apply the replacements in the keywords() hash to the @a script. /// @note Do not call this directly, use Queue::replaceKeywords instead. void replaceKeywords(QString &launchScript) const; }; } // end namespace MoleQueue Q_DECLARE_METATYPE(MoleQueue::Job) #endif // MOLEQUEUE_JOB_H molequeue-0.9.0/molequeue/app/jobactionfactories/000077500000000000000000000000001323436134600221575ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/jobactionfactories/killjobactionfactory.cpp000066400000000000000000000064421323436134600271050ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "killjobactionfactory.h" #include "../job.h" #include "../queue.h" #include "../queuemanager.h" #include "../server.h" #include #include Q_DECLARE_METATYPE(QList) namespace MoleQueue { KillJobActionFactory::KillJobActionFactory() : JobActionFactory() { qRegisterMetaType >("QList"); m_isMultiJob = true; m_flags |= JobActionFactory::ContextItem; } KillJobActionFactory::~KillJobActionFactory() { } bool KillJobActionFactory::isValidForJob(const Job &job) const { switch (job.jobState()) { case MoleQueue::Accepted: case MoleQueue::QueuedLocal: case MoleQueue::Submitted: case MoleQueue::QueuedRemote: case MoleQueue::RunningLocal: case MoleQueue::RunningRemote: case MoleQueue::Error: return true; case MoleQueue::Unknown: case MoleQueue::None: case MoleQueue::Finished: case MoleQueue::Canceled: default: break; } return false; } QList KillJobActionFactory::createActions() { QList result; QAction *newAction = NULL; if (m_attemptedJobAdditions == 1 && m_jobs.size() == 1) { newAction = new QAction (tr("Cancel job '%1'...") .arg(m_jobs.first().description()), NULL); } else if (m_attemptedJobAdditions > 1) { newAction = new QAction (NULL); if (static_cast(m_jobs.size()) == m_attemptedJobAdditions) { newAction->setText(tr("Cancel %1 jobs...").arg(m_jobs.size())); } else { newAction->setText(tr("Cancel %1 of %2 selected jobs...") .arg(m_jobs.size()).arg(m_attemptedJobAdditions)); } } if (newAction) { newAction->setData(QVariant::fromValue(m_jobs)); connect(newAction, SIGNAL(triggered()), this, SLOT(actionTriggered())); result << newAction; } return result; } void KillJobActionFactory::actionTriggered() { QAction *action = qobject_cast(sender()); if (!action) return; // The sender was a QAction. Is its data a list of jobs? QList jobs = action->data().value >(); if (!jobs.size()) return; QMessageBox::StandardButton confirm = QMessageBox::question(NULL, tr("Really cancel jobs?"), tr("Are you sure you would like to cancel %n " "job(s)? ", "", jobs.size()), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (confirm != QMessageBox::Yes) return; foreach (const Job &job, jobs) { Queue *queue = m_server->queueManager()->lookupQueue(job.queue()); if (queue) queue->killJob(job); } } } // namespace MoleQueue molequeue-0.9.0/molequeue/app/jobactionfactories/killjobactionfactory.h000066400000000000000000000023551323436134600265510ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_KILLJOBACTIONFACTORY_H #define MOLEQUEUE_KILLJOBACTIONFACTORY_H #include "../jobactionfactory.h" namespace MoleQueue { /// @brief JobActionFactory for canceling jobs. class KillJobActionFactory : public JobActionFactory { Q_OBJECT public: KillJobActionFactory(); ~KillJobActionFactory(); QString name() const { return tr("Kill job"); } bool isValidForJob(const Job &job) const; QList createActions(); unsigned int usefulness() const { return 200; } public slots: virtual void actionTriggered(); }; } // namespace MoleQueue #endif // MOLEQUEUE_KILLJOBACTIONFACTORY_H molequeue-0.9.0/molequeue/app/jobactionfactories/opendirectoryactionfactory.cpp000066400000000000000000000054021323436134600303400ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "opendirectoryactionfactory.h" #include "../job.h" #include #include #include Q_DECLARE_METATYPE(QList) namespace MoleQueue { OpenDirectoryActionFactory::OpenDirectoryActionFactory() : JobActionFactory() { qRegisterMetaType >("QList"); qRegisterMetaType >("QList"); m_isMultiJob = true; m_flags |= JobActionFactory::ContextItem; } OpenDirectoryActionFactory::~OpenDirectoryActionFactory() { } bool OpenDirectoryActionFactory::isValidForJob(const Job &job) const { return job.isValid() && !job.outputDirectory().isEmpty(); } QList OpenDirectoryActionFactory::createActions() { QList result; if (m_attemptedJobAdditions == 1) { QAction *newAction = new QAction ( tr("Open '%1' in file browser...").arg(m_jobs.first().description()), NULL); newAction->setData(QVariant::fromValue(m_jobs)); connect(newAction, SIGNAL(triggered()), this, SLOT(actionTriggered())); result << newAction; } else if (m_attemptedJobAdditions > 1) { QAction *newAction = new QAction (NULL); if (static_cast(m_jobs.size()) == m_attemptedJobAdditions) { newAction->setText(tr("Open %1 jobs in file browser") .arg(m_jobs.size())); } else { newAction->setText(tr("Open %1 of %2 selected jobs in file browser...") .arg(m_jobs.size()).arg(m_attemptedJobAdditions)); } newAction->setData(QVariant::fromValue(m_jobs)); connect(newAction, SIGNAL(triggered()), this, SLOT(actionTriggered())); result << newAction; } return result; } void OpenDirectoryActionFactory::actionTriggered() { QAction *action = qobject_cast(sender()); if (!action) return; // The sender was a QAction. Is its data a list of jobs? QList jobs = action->data().value >(); if (!jobs.size()) return; foreach (const Job &job, jobs) { if (job.isValid()) QDesktopServices::openUrl(QUrl::fromLocalFile(job.outputDirectory())); } } } // end namespace MoleQueue molequeue-0.9.0/molequeue/app/jobactionfactories/opendirectoryactionfactory.h000066400000000000000000000024141323436134600300050ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef OPENDIRECTORYACTIONFACTORY_H #define OPENDIRECTORYACTIONFACTORY_H #include "../jobactionfactory.h" namespace MoleQueue { /// JobActionFactory subclass to open job output in a file browser. class OpenDirectoryActionFactory : public JobActionFactory { Q_OBJECT public: OpenDirectoryActionFactory(); ~OpenDirectoryActionFactory(); QString name() const { return tr("Open directory"); } bool isValidForJob(const Job &job) const; QList createActions(); unsigned int usefulness() const { return 300; } protected slots: void actionTriggered(); }; } // end namespace MoleQueue #endif // OPENDIRECTORYACTIONFACTORY_H molequeue-0.9.0/molequeue/app/jobactionfactories/openwithactionfactory.cpp000066400000000000000000000335731323436134600273210ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012-2013 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "openwithactionfactory.h" #include "molequeueconfig.h" #include "../client/jsonrpcclient.h" #include "../job.h" #include "../logger.h" #include "../program.h" #include "../queue.h" #include "../queuemanager.h" #include "../server.h" #include #include #include #include #include #include #include #include #include #include #include namespace MoleQueue { //============================================================================== // HandlerStrategy interface class OpenWithActionFactory::HandlerStrategy { public: virtual ~HandlerStrategy() {}; virtual HandlerStrategy* clone() const = 0; virtual void readSettings(QSettings &settings) = 0; virtual void writeSettings(QSettings &settings) const = 0; virtual bool openFile(const QString &fileName, const QString &workDir) = 0; virtual QString openFileError() const { return m_error; } protected: QString m_error; }; namespace { //============================================================================== // ExecutableHandlerStrategy implementation class ExecutableHandlerStrategy : public OpenWithActionFactory::HandlerStrategy { QString m_executable; public: explicit ExecutableHandlerStrategy(const QString &exec = QString()); HandlerStrategy* clone() const; void readSettings(QSettings &settings); void writeSettings(QSettings &settings) const; bool openFile(const QString &fileName, const QString &workDir); QString executable() const { return m_executable; } }; ExecutableHandlerStrategy::ExecutableHandlerStrategy(const QString &exec) : m_executable(exec) { } OpenWithActionFactory::HandlerStrategy* ExecutableHandlerStrategy::clone() const { return new ExecutableHandlerStrategy(m_executable); } void ExecutableHandlerStrategy::readSettings(QSettings &settings) { m_executable = settings.value("executable").toString(); } void ExecutableHandlerStrategy::writeSettings(QSettings &settings) const { settings.setValue("executable", m_executable); } bool ExecutableHandlerStrategy::openFile(const QString &fileName, const QString &workDir) { m_error.clear(); qint64 pid = -1; bool ok = QProcess::startDetached(QString("%1").arg(m_executable), QStringList() << fileName, workDir, &pid); // pid may be set to zero in certain cases in the UNIX QProcess implementation if (ok && pid >= 0) { Logger::logDebugMessage( Logger::tr("Running '%1 %2' in '%3' (PID=%4)", "1 is an executable, 2 is a filename, 3 is a " "directory, and 4 is a process id.") .arg(m_executable).arg(fileName).arg(workDir) .arg(QString::number(pid))); return true; } m_error = Logger::tr("Error while starting '%1 %2' in '%3'", "1 is an executable, 2 is a filename, 3 is a directory.") .arg(m_executable).arg(fileName).arg(workDir); return false; } //============================================================================== // RpcHandlerStrategy implementation class RpcHandlerStrategy : public OpenWithActionFactory::HandlerStrategy { public: explicit RpcHandlerStrategy(const QString &server = QString(), const QString &method = QString()); HandlerStrategy* clone() const; void readSettings(QSettings &settings); void writeSettings(QSettings &settings) const; bool openFile(const QString &fileName, const QString &workDir); QString rpcServer() const { return m_rpcServer; } QString rpcMethod() const { return m_rpcMethod; } private: QString m_rpcServer; QString m_rpcMethod; }; RpcHandlerStrategy::RpcHandlerStrategy(const QString &server, const QString &method) : m_rpcServer(server), m_rpcMethod(method) { } OpenWithActionFactory::HandlerStrategy* RpcHandlerStrategy::clone() const { return new RpcHandlerStrategy(m_rpcServer, m_rpcMethod); } void RpcHandlerStrategy::readSettings(QSettings &settings) { m_rpcServer = settings.value("rpcServer").toString(); m_rpcMethod = settings.value("rpcMethod").toString(); } void RpcHandlerStrategy::writeSettings( QSettings &settings) const { settings.setValue("rpcServer", m_rpcServer); settings.setValue("rpcMethod", m_rpcMethod); } bool RpcHandlerStrategy::openFile(const QString &fileName, const QString &workDir) { Q_UNUSED(workDir); m_error.clear(); QEventLoop loop; JsonRpcClient client; if (!client.connectToServer(m_rpcServer)) { m_error = Logger::tr("Unable to connect to RPC server at '%1'.") .arg(m_rpcServer); return false; } QObject::connect(&client, SIGNAL(resultReceived(QJsonObject)), &loop, SLOT(quit())); QTimer timer; timer.setSingleShot(true); QObject::connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit())); QJsonObject req(client.emptyRequest()); req["method"] = m_rpcMethod; QJsonObject params; params["fileName"] = fileName; req["params"] = params; if (!client.sendRequest(req)) { m_error = Logger::tr("Cannot send request to RPC server at '%1'") .arg(m_rpcServer); return false; } timer.start(3000); loop.exec(); if (!timer.isActive()) { m_error = Logger::tr("Timeout waiting for a reply from RPC server '%1'") .arg(m_rpcServer); return false; } return true; } } // end anon namespace //============================================================================== // OpenWithActionFactory implementation: OpenWithActionFactory::OpenWithActionFactory() : JobActionFactory(), m_handlerType(NoHandler), m_handler(NULL) { qRegisterMetaType("Job"); m_isMultiJob = false; m_flags |= JobActionFactory::ContextItem; } OpenWithActionFactory::~OpenWithActionFactory() { delete m_handler; } OpenWithActionFactory::OpenWithActionFactory(const OpenWithActionFactory &other) : JobActionFactory(other), m_name(other.m_name), m_menuText(other.m_menuText), m_handlerType(other.m_handlerType), m_handler(other.m_handler == NULL ? NULL : other.m_handler->clone()), m_filePatterns(other.m_filePatterns), m_fileNames(other.m_fileNames) { } OpenWithActionFactory & OpenWithActionFactory::operator=(OpenWithActionFactory other) { JobActionFactory::operator=(other); using namespace std; swap(m_handlerType, other.m_handlerType); swap(m_handler, other.m_handler); swap(m_filePatterns, other.m_filePatterns); swap(m_fileNames, other.m_fileNames); return *this; } void OpenWithActionFactory::readSettings(QSettings &settings) { JobActionFactory::readSettings(settings); m_name = settings.value("name").toString(); setHandlerType(static_cast( settings.value("handlerType", NoHandler).toInt())); settings.beginGroup("handler"); if (m_handler) m_handler->readSettings(settings); settings.endGroup(); m_filePatterns.clear(); int numPatterns = settings.beginReadArray("patterns"); for (int i = 0; i < numPatterns; ++i) { settings.setArrayIndex(i); m_filePatterns << settings.value("regexp").toRegExp(); } settings.endArray(); } void OpenWithActionFactory::writeSettings(QSettings &settings) const { JobActionFactory::writeSettings(settings); settings.setValue("name", m_name); settings.setValue("handlerType", m_handlerType); settings.beginGroup("handler"); settings.remove(""); // clear any old handler settings if (m_handler) m_handler->writeSettings(settings); settings.endGroup(); settings.beginWriteArray("patterns", m_filePatterns.size()); for (int i = 0; i < m_filePatterns.size(); ++i) { settings.setArrayIndex(i); settings.setValue("regexp", m_filePatterns[i]); } settings.endArray(); } bool OpenWithActionFactory::isValidForJob(const Job &job) const { if (!job.isValid()) return false; QDir directory(job.jobState() == Finished ? job.outputDirectory() : job.localWorkingDirectory()); return scanDirectoryForRecognizedFiles(directory, directory); } void OpenWithActionFactory::clearJobs() { JobActionFactory::clearJobs(); m_fileNames.clear(); m_menuText = QString(); } bool OpenWithActionFactory::useMenu() const { return true; } QString OpenWithActionFactory::menuText() const { return m_menuText; } QList OpenWithActionFactory::createActions() { QList result; if (m_attemptedJobAdditions == 1 && m_jobs.size() == 1) { const Job &job = m_jobs.first(); m_menuText = tr("Open '%1' with %2") .arg(job.description(), name()); QStringList fileNames = m_fileNames.keys(); foreach (const QString &fileName, fileNames) { QAction *newAction = new QAction(fileName, NULL); newAction->setData(QVariant::fromValue(job)); newAction->setProperty("fileName", m_fileNames.value(fileName)); connect(newAction, SIGNAL(triggered()), this, SLOT(actionTriggered())); result << newAction; } } return result; } unsigned int OpenWithActionFactory::usefulness() const { return 800; } void OpenWithActionFactory::setHandlerType( OpenWithActionFactory::HandlerType type) { if (type == m_handlerType) return; delete m_handler; switch (m_handlerType = type) { case ExecutableHandler: m_handler = new ExecutableHandlerStrategy; break; case RpcHandler: m_handler = new RpcHandlerStrategy; break; default: case NoHandler: m_handler = NULL; break; } } void OpenWithActionFactory::setExecutable(const QString &exec) { delete m_handler; m_handler = new ExecutableHandlerStrategy(exec); m_handlerType = ExecutableHandler; } QString OpenWithActionFactory::executable() const { return m_handlerType == ExecutableHandler ? static_cast(m_handler)->executable() : QString(); } void OpenWithActionFactory::setRpcDetails(const QString &myRpcServer, const QString &myRpcMethod) { delete m_handler; m_handler = new RpcHandlerStrategy(myRpcServer, myRpcMethod); m_handlerType = RpcHandler; } QString OpenWithActionFactory::rpcServer() const { return m_handlerType == RpcHandler ? static_cast(m_handler)->rpcServer() : QString(); } QString OpenWithActionFactory::rpcMethod() const { return m_handlerType == RpcHandler ? static_cast(m_handler)->rpcMethod() : QString(); } QList OpenWithActionFactory::filePatterns() const { return m_filePatterns; } QList &OpenWithActionFactory::filePatternsRef() { return m_filePatterns; } const QList &OpenWithActionFactory::filePatternsRef() const { return m_filePatterns; } void OpenWithActionFactory::setFilePatterns(const QList &patterns) { m_filePatterns = patterns; } void OpenWithActionFactory::actionTriggered() { QAction *action = qobject_cast(sender()); if (!action) { Logger::logWarning(tr("OpenWithActionFactory::actionTriggered: Sender is " "not a QAction!")); return; } // The sender was a QAction. Is its data a job? Job job = action->data().value(); if (!job.isValid()) { Logger::logWarning(tr("OpenWithActionFactory::actionTriggered: Action data " "is not a Job.")); return; } // Filename was set? QString fileName = action->property("fileName").toString(); if (!QFileInfo(fileName).exists()) { Logger::logWarning(tr("OpenWithActionFactory::actionTriggered: No filename " "associated with job."), job.moleQueueId()); return; } // Is the handler set? if (!m_handler) { Logger::logWarning(tr("OpenWithActionFactory::actionTriggered: No handler " "set."), job.moleQueueId()); return; } // Setup the progress dialog QProgressDialog dialog; dialog.setMinimumDuration(500); dialog.setRange(0, 0); dialog.setValue(0); // Should be ready to go! QString workDir = QFileInfo(fileName).absolutePath(); if (!m_handler->openFile(fileName, workDir)) { QString err(tr("Error: %1").arg(m_handler->openFileError())); Logger::logWarning(err, job.moleQueueId()); QMessageBox::critical(NULL, tr("Cannot start process"), err); } } bool OpenWithActionFactory::scanDirectoryForRecognizedFiles( const QDir &baseDir, const QDir &dir) const { bool result = false; // Recursively check subdirectories QStringList subDirs(dir.entryList(QDir::Dirs | QDir::Readable | QDir::NoDotAndDotDot, QDir::Name)); foreach (const QString &subDir, subDirs) { if (scanDirectoryForRecognizedFiles(baseDir, QDir(dir.absoluteFilePath(subDir)))) result = true; } QStringList entries(dir.entryList(QDir::Files | QDir::Readable, QDir::Name)); foreach (const QString &fileName, entries) { foreach (const QRegExp ®exp, m_filePatterns) { if (regexp.indexIn(fileName) >= 0) { result = true; m_fileNames.insert( baseDir.relativeFilePath(dir.absoluteFilePath(fileName)), dir.absoluteFilePath(fileName)); } } } return result; } } // end namespace MoleQueue molequeue-0.9.0/molequeue/app/jobactionfactories/openwithactionfactory.h000066400000000000000000000127711323436134600267630ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012-2013 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_OPENWITHACTIONFACTORY_H #define MOLEQUEUE_OPENWITHACTIONFACTORY_H #include "../jobactionfactory.h" #include "molequeueglobal.h" class QDir; namespace MoleQueue { /** * @class OpenWithActionFactory openwithactionfactory.h * * @brief The OpenWithActionFactory class provides a generic mechanism for * performing an action on a file in a job's directory. It is configured to * process a file by calling an external executable or sending RPC requests. * * The OpenWithActionFactory allows arbitrary actions to be performed on files * in a job's directory. A list of QRegExp objects is used to filter filenames * so that the factory only produces actions for files that match one of the * filePatterns(). * * The actions will either call an external executable() to handle the file, * or send a request (rpcMethod()) to a JSON-RPC 2.0 server (rpcServer()). The * executable will be run as: ~~~ executable /absolute/path/to/selected/fileName ~~~ * * RPC requests will be of the form: ~~~ { "jsonrpc": "2.0", "method": "rpcMethod", "params": { "fileName": "/absolute/path/to/selected/fileName" } }, "id": "XXX" } ~~~ * Use setExecutable() to set the actions to use an executable, or * setRpcDetails() to use RPC calls. The type of file handler can be checked * with handlerType(). */ class OpenWithActionFactory : public JobActionFactory { Q_OBJECT public: /** * The HandlerType enum identifies types of file handling strategies. * @sa handlerType() */ enum HandlerType { /** *No handler specified. */ NoHandler = -1, /** * Open the file with an external executable. * @sa setExecutable(). */ ExecutableHandler = 0, /** * Open the file with a JSON-RPC request. * @sa setRpcDetails(). */ RpcHandler }; /** * Construct a new, uninitialized OpenWithActionFactory. */ OpenWithActionFactory(); virtual ~OpenWithActionFactory(); /** * Construct a copy of the OpenWithActionFactory @a other. */ OpenWithActionFactory(const OpenWithActionFactory &other); /** * Copy the OpenWithActionFactory @a other into @a this. */ OpenWithActionFactory & operator=(OpenWithActionFactory other); /** * Save/restore state. @{ */ void readSettings(QSettings &settings); void writeSettings(QSettings &settings) const; /** @} */ /** * The user-friendly GUI name of this action. Used to set the action menu text * to "Open '[job description]' with '[name()]'". @{ */ QString name() const { return m_name; } void setName(const QString &n) { m_name = n; } /** @} */ // Reimplemented virtuals: bool isValidForJob(const Job &job) const; void clearJobs(); bool useMenu() const; QString menuText() const; QList createActions(); unsigned int usefulness() const; /** * The type of file handling strategy to use. * @sa HandlerType * @sa setExecutable() setRpcDetails() * @{ */ void setHandlerType(HandlerType type); HandlerType handlerType() const { return m_handlerType; } /** @} */ /** * Produce actions that execute @a exec on the selected file as: ~~~ executable /absolute/path/to/selected/fileName ~~~ * * @note Calling setExecutable() erases the rpcServer() and rpcMethod() * values. * @{ */ void setExecutable(const QString &exec); QString executable() const; /** @} */ /** * Produce actions that set JSON-RPC 2.0 requests to a local socket server * named @a myRpcServer of the form: ~~~ { "jsonrpc": "2.0", "method": "myRpcMethod", "params": { "fileName": "/absolute/path/to/selected/fileName" } }, "id": "XXX" } ~~~ * @note This method erases the executable() value. */ void setRpcDetails(const QString &myRpcServer, const QString &myRpcMethod); /** * @return The target JSON-RPC server socket name. */ QString rpcServer() const; /** * @return The method to use in JSON-RPC requests. */ QString rpcMethod() const; /** * A list of QRegExp objects that match files supported by the file handler. * An action will be produce for each file that matches any of these QRegExps. * @{ */ QList filePatterns() const; QList& filePatternsRef(); const QList& filePatternsRef() const; void setFilePatterns(const QList &patterns); /** @} */ class HandlerStrategy; private slots: void actionTriggered(); private: bool scanDirectoryForRecognizedFiles(const QDir &baseDir, const QDir &dir) const; QString m_name; QString m_menuText; HandlerType m_handlerType; HandlerStrategy *m_handler; QList m_filePatterns; mutable QMap m_fileNames; // GUI name: absolute file path }; } // end namespace MoleQueue #endif // MOLEQUEUE_OPENWITHACTIONFACTORY_H molequeue-0.9.0/molequeue/app/jobactionfactories/removejobactionfactory.cpp000066400000000000000000000057631323436134600274540ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "removejobactionfactory.h" #include "../job.h" #include "../jobmanager.h" #include "../server.h" #include #include Q_DECLARE_METATYPE(QList) namespace MoleQueue { RemoveJobActionFactory::RemoveJobActionFactory() : JobActionFactory() { qRegisterMetaType >("QList"); m_isMultiJob = true; m_flags |= JobActionFactory::ContextItem; } RemoveJobActionFactory::~RemoveJobActionFactory() { } bool RemoveJobActionFactory::isValidForJob(const Job &job) const { return job.isValid() && m_server->jobManager()->indexOf(job) != -1; } QList RemoveJobActionFactory::createActions() { QList result; if (m_attemptedJobAdditions == 1 && m_jobs.size() == 1) { QAction *newAction = new QAction ( tr("Remove '%1'...").arg(m_jobs.first().description()), NULL); newAction->setData(QVariant::fromValue(m_jobs)); connect(newAction, SIGNAL(triggered()), this, SLOT(actionTriggered())); result << newAction; } else if (m_attemptedJobAdditions > 1) { QAction *newAction = new QAction (NULL); if (static_cast(m_jobs.size()) == m_attemptedJobAdditions) { newAction->setText(tr("Remove %1 jobs...").arg(m_jobs.size())); } else { newAction->setText(tr("Remove %1 of %2 selected jobs...") .arg(m_jobs.size()).arg(m_attemptedJobAdditions)); } newAction->setData(QVariant::fromValue(m_jobs)); connect(newAction, SIGNAL(triggered()), this, SLOT(actionTriggered())); result << newAction; } return result; } void RemoveJobActionFactory::actionTriggered() { QAction *action = qobject_cast(sender()); if (!action) return; // The sender was a QAction. Is its data a list of jobs? QList jobs = action->data().value >(); if (!jobs.size()) return; QMessageBox::StandardButton confirm = QMessageBox::question(NULL, tr("Really remove jobs?"), tr("Are you sure you would like to remove %n job(s)? " "This will not delete any input or output files.", "", jobs.size()), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (confirm != QMessageBox::Yes) return; m_server->jobManager()->removeJobs(jobs); } } // namespace MoleQueue molequeue-0.9.0/molequeue/app/jobactionfactories/removejobactionfactory.h000066400000000000000000000026701323436134600271130ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_REMOVEJOBACTIONFACTORY_H #define MOLEQUEUE_REMOVEJOBACTIONFACTORY_H #include "../jobactionfactory.h" namespace MoleQueue { /** * @class RemoveJobActionFactory removejobactionfactory.h * * @brief JobActionFactory subclass which removes jobs from the Server * JobManager. * @author David C. Lonie */ class RemoveJobActionFactory : public JobActionFactory { Q_OBJECT public: RemoveJobActionFactory(); ~RemoveJobActionFactory(); QString name() const { return tr("Remove job"); } bool isValidForJob(const Job &job) const; QList createActions(); unsigned int usefulness() const { return 200; } public slots: virtual void actionTriggered(); }; } // namespace MoleQueue #endif // MOLEQUEUE_REMOVEJOBACTIONFACTORY_H molequeue-0.9.0/molequeue/app/jobactionfactories/viewjoblogactionfactory.cpp000066400000000000000000000055461323436134600276320ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "viewjoblogactionfactory.h" #include "logwindow.h" #include "job.h" #include Q_DECLARE_METATYPE(QList) namespace MoleQueue { ViewJobLogActionFactory::ViewJobLogActionFactory() : MoleQueue::JobActionFactory(), m_logWindowParent(NULL) { qRegisterMetaType >("QList"); m_isMultiJob = false; m_flags |= JobActionFactory::ContextItem; } ViewJobLogActionFactory::~ViewJobLogActionFactory() { qDeleteAll(m_windowMap.values()); m_windowMap.clear(); } bool ViewJobLogActionFactory::isValidForJob(const Job &job) const { return job.isValid(); } QList ViewJobLogActionFactory::createActions() { QList result; QAction *newAction = NULL; if (m_attemptedJobAdditions == 1 && m_jobs.size() == 1) { newAction = new QAction (tr("View log for job '%1'...") .arg(m_jobs.first().description()), NULL); } if (newAction) { newAction->setData(QVariant::fromValue(m_jobs)); connect(newAction, SIGNAL(triggered()), this, SLOT(actionTriggered())); result << newAction; } return result; } void ViewJobLogActionFactory::setLogWindowParent(QWidget *widgy) { m_logWindowParent = widgy; } void ViewJobLogActionFactory::actionTriggered() { QAction *action = qobject_cast(sender()); if (!action) return; // The sender was a QAction. Is its data a list of jobs? QList jobs = action->data().value >(); if (jobs.size() != 1 || !jobs.first().isValid()) return; IdType moleQueueId = jobs.first().moleQueueId(); LogWindow *logWindow = m_windowMap.value(moleQueueId, NULL); if (!logWindow) { logWindow = new LogWindow(m_logWindowParent, moleQueueId); m_windowMap.insert(moleQueueId, logWindow); connect(logWindow, SIGNAL(aboutToClose()), this, SLOT(removeSenderFromMap())); } logWindow->show(); logWindow->raise(); } void ViewJobLogActionFactory::removeSenderFromMap() { LogWindow *deadWindow = qobject_cast(this->sender()); if (!deadWindow) return; IdType moleQueueId = m_windowMap.key(deadWindow, InvalidId); if (moleQueueId != InvalidId) m_windowMap.remove(moleQueueId); deadWindow->deleteLater(); } } // namespace MoleQueue molequeue-0.9.0/molequeue/app/jobactionfactories/viewjoblogactionfactory.h000066400000000000000000000027571323436134600273000ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_VIEWJOBLOGACTIONFACTORY_H #define MOLEQUEUE_VIEWJOBLOGACTIONFACTORY_H #include "jobactionfactory.h" #include "molequeueglobal.h" namespace MoleQueue { class LogWindow; /// @brief JobActionFactory for opening log windows filtered to specified jobs. class ViewJobLogActionFactory : public MoleQueue::JobActionFactory { Q_OBJECT public: ViewJobLogActionFactory(); ~ViewJobLogActionFactory(); QString name() const { return tr("View log"); } bool isValidForJob(const Job &job) const; QList createActions(); void setLogWindowParent(QWidget *widgy); unsigned int usefulness() const { return 50; } public slots: virtual void actionTriggered(); void removeSenderFromMap(); private: QWidget *m_logWindowParent; QMap m_windowMap; }; } // namespace MoleQueue #endif // MOLEQUEUE_VIEWJOBLOGACTIONFACTORY_H molequeue-0.9.0/molequeue/app/jobactionfactory.cpp000066400000000000000000000047011323436134600223550ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "jobactionfactory.h" #include "job.h" namespace MoleQueue { JobActionFactory::JobActionFactory() : QObject(), m_attemptedJobAdditions(0), m_isMultiJob(false), m_server(NULL) { } JobActionFactory::JobActionFactory(const JobActionFactory &other) : QObject(), m_attemptedJobAdditions(other.m_attemptedJobAdditions), m_isMultiJob(other.m_isMultiJob), m_server(other.m_server), m_jobs(other.m_jobs), m_flags(other.m_flags) { } JobActionFactory::~JobActionFactory() { } JobActionFactory &JobActionFactory::operator =(const JobActionFactory &other) { m_attemptedJobAdditions = other.m_attemptedJobAdditions; m_isMultiJob = other.m_isMultiJob; m_server = other.m_server; m_jobs = other.m_jobs; m_flags = other.m_flags; return *this; } void JobActionFactory::readSettings(QSettings &settings) { m_isMultiJob = settings.value("isMultiJob").toBool(); m_flags = static_cast(settings.value("flags").toInt()); } void JobActionFactory::writeSettings(QSettings &settings) const { settings.setValue("isMultiJob", m_isMultiJob); settings.setValue("flags", static_cast(m_flags)); } void JobActionFactory::clearJobs() { m_attemptedJobAdditions = 0; m_jobs.clear(); } bool JobActionFactory::isMultiJob() const { return m_isMultiJob; } bool JobActionFactory::addJobIfValid(const Job &job) { ++m_attemptedJobAdditions; bool result = isValidForJob(job); if (result) m_jobs.append(job); return result; } bool JobActionFactory::useMenu() const { return false; } QString JobActionFactory::menuText() const { return QString(); } bool JobActionFactory::hasValidActions() const { return static_cast(m_jobs.size()); } JobActionFactory::Flags JobActionFactory::flags() const { return m_flags; } void JobActionFactory::setFlags(JobActionFactory::Flags f) { m_flags = f; } } molequeue-0.9.0/molequeue/app/jobactionfactory.h000066400000000000000000000077461323436134600220360ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef JOBCONTEXTACTIONFACTORY_H #define JOBCONTEXTACTIONFACTORY_H #include #include class QAction; namespace MoleQueue { class Job; class Server; /** * @class JobActionFactory jobactionfactory.h * @brief Base class for implementing a factory which creates QActions that * operate on Job instances. * @author David C. Lonie */ class JobActionFactory : public QObject { Q_OBJECT public: /// Flags defining properties of the created QActions enum Flag { /// Actions may be used as a context menu item. ContextItem = 0x1, }; Q_DECLARE_FLAGS(Flags, Flag) JobActionFactory(); JobActionFactory(const JobActionFactory &other); ~JobActionFactory(); JobActionFactory & operator=(const JobActionFactory &other); virtual void readSettings(QSettings &settings); virtual void writeSettings(QSettings &settings) const; /** * Set the Server instance. Called in ActionFactoryManager::addFactory(). */ void setServer(Server *s) { m_server = s; } /** Return the Server instance. */ Server * server() const { return m_server; } /** * A name that uniquely identifies this factory. */ virtual QString name() const = 0; /** Clear m_jobs and reset m_attemptedJobAdditions */ virtual void clearJobs(); /** @return true if the product QActions operate on multiple jobs. */ virtual bool isMultiJob() const; /** * Increment m_attemptedJobAdditions and check if the factory's actions are * appropriate for the Job @job by calling isValidForJob. If so, @a job is * added to m_jobs. * @sa isValidForJob */ virtual bool addJobIfValid(const Job &job); /** * @return true if the factory's actions are appropriate for @a job. */ virtual bool isValidForJob(const Job &job) const = 0; /** * @return true if this factory's actions should be placed in a submenu. Use * menuText() to get the menu name. */ virtual bool useMenu() const; /** * @return The text to be used for a submenu containing this factory's items. * Call useMenu() to see if this is required. */ virtual QString menuText() const; /** * @return true if addJobIfValid has been called with any appropriate jobs * since the last call to clearJobs(). */ virtual bool hasValidActions() const; /** * Create actions that operate on the Job objects in m_jobs. The caller is * responsible for managing the lifetime of the actions (passing them to a * QMenu or similar is usually sufficient). * @sa hasValidActions() * @sa addJobIfValid * @sa clearJobs */ virtual QList createActions() = 0; /** * The "usefulness" of the actions produced by this factory, used to order * actions in generated menus, etc. Lower value means higher usefulness. */ virtual unsigned int usefulness() const = 0; /** * @return A set of JobActionFactory::Flags describe the actions produced by * this factory. */ virtual Flags flags() const; /** * @return Set JobActionFactory::Flags describing the actions produced by * this factory. */ virtual void setFlags(Flags f); protected: unsigned int m_attemptedJobAdditions; bool m_isMultiJob; Server *m_server; QList m_jobs; Flags m_flags; }; Q_DECLARE_OPERATORS_FOR_FLAGS(JobActionFactory::Flags) } // end namespace MoleQueue #endif // JOBCONTEXTACTIONFACTORY_H molequeue-0.9.0/molequeue/app/jobdata.cpp000066400000000000000000000225741323436134600204310ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "jobdata.h" #include "jobmanager.h" #include "logger.h" #include #include #include #include namespace MoleQueue { JobData::JobData(JobManager *parentManager) : m_jobManager(parentManager), m_jobState(MoleQueue::None), m_cleanRemoteFiles(false), m_retrieveOutput(true), m_cleanLocalWorkingDirectory(false), m_hideFromGui(false), m_popupOnStateChange(false), m_numberOfCores(DEFAULT_NUM_CORES), m_maxWallTime(-1), // use default queue time m_moleQueueId(InvalidId), m_queueId(InvalidId), m_needsSync(true) { } JobData::JobData(const MoleQueue::JobData &other) : m_jobManager(other.m_jobManager), m_queue(other.m_queue), m_program(other.m_program), m_jobState(other.m_jobState), m_description(other.m_description), m_inputFile(other.m_inputFile), m_additionalInputFiles(other.m_additionalInputFiles), m_outputDirectory(other.m_outputDirectory), m_localWorkingDirectory(other.m_localWorkingDirectory), m_cleanRemoteFiles(other.m_cleanRemoteFiles), m_retrieveOutput(other.m_retrieveOutput), m_cleanLocalWorkingDirectory(other.m_cleanLocalWorkingDirectory), m_hideFromGui(other.m_hideFromGui), m_popupOnStateChange(other.m_popupOnStateChange), m_numberOfCores(other.m_numberOfCores), m_maxWallTime(other.m_maxWallTime), m_moleQueueId(other.m_moleQueueId), m_queueId(other.m_queueId), m_needsSync(true) { } QJsonObject JobData::toJsonObject() const { QJsonObject result; result.insert("queue", m_queue); result.insert("program", m_program); result.insert("jobState", QLatin1String(jobStateToString(m_jobState))); result.insert("description", m_description); result.insert("inputFile", m_inputFile.toJsonObject()); if (!m_additionalInputFiles.isEmpty()) { QJsonArray additionalFiles; foreach (const FileSpecification &spec, m_additionalInputFiles) additionalFiles.append(spec.toJsonObject()); result.insert("additionalInputFiles", additionalFiles); } result.insert("outputDirectory", m_outputDirectory); result.insert("localWorkingDirectory", m_localWorkingDirectory); result.insert("cleanRemoteFiles", m_cleanRemoteFiles); result.insert("retrieveOutput", m_retrieveOutput); result.insert("cleanLocalWorkingDirectory", m_cleanLocalWorkingDirectory); result.insert("hideFromGui", m_hideFromGui); result.insert("popupOnStateChange", m_popupOnStateChange); result.insert("numberOfCores", m_numberOfCores); result.insert("maxWallTime", m_maxWallTime); result.insert("moleQueueId", idTypeToJson(m_moleQueueId)); result.insert("queueId", idTypeToJson(m_queueId)); if (!m_keywords.isEmpty()) { QJsonObject keywords_; foreach (const QString &key, m_keywords.keys()) keywords_.insert(key, m_keywords.value(key)); result.insert("keywords", keywords_); } return result; } void JobData::setFromJson(const QJsonObject &state) { if (state.contains("queue")) m_queue = state.value("queue").toString(); if (state.contains("program")) m_program = state.value("program").toString(); if (state.contains("description")) m_description = state.value("description").toString(); if (state.contains("jobState")) m_jobState = stringToJobState(state.value("jobState").toString()); if (state.contains("inputFile")) m_inputFile = FileSpecification(state.value("inputFile").toObject()); m_additionalInputFiles.clear(); if (state.contains("additionalInputFiles")) { foreach(const QJsonValue &inputFile_, state.value("additionalInputFiles").toArray()) { m_additionalInputFiles.append(FileSpecification(inputFile_.toObject())); } } if (state.contains("outputDirectory")) m_outputDirectory = state.value("outputDirectory").toString(); if (state.contains("localWorkingDirectory")) m_localWorkingDirectory = state.value("localWorkingDirectory").toString(); if (state.contains("cleanRemoteFiles")) m_cleanRemoteFiles = state.value("cleanRemoteFiles").toBool(); if (state.contains("retrieveOutput")) m_retrieveOutput = state.value("retrieveOutput").toBool(); if (state.contains("cleanLocalWorkingDirectory")) { m_cleanLocalWorkingDirectory = state.value("cleanLocalWorkingDirectory").toBool(); } if (state.contains("hideFromGui")) m_hideFromGui = state.value("hideFromGui").toBool(); if (state.contains("popupOnStateChange")) m_popupOnStateChange = state.value("popupOnStateChange").toBool(); if (state.contains("numberOfCores")) m_numberOfCores = static_cast(state.value("numberOfCores").toDouble()); if (state.contains("maxWallTime")) m_maxWallTime = static_cast(state.value("maxWallTime").toDouble()); if (state.contains("moleQueueId")) m_moleQueueId = toIdType(state.value("moleQueueId")); if (state.contains("queueId")) m_queueId = toIdType(state.value("queueId")); if (state.contains("keywords")) { m_keywords.clear(); QJsonObject keywords_ = state.value("keywords").toObject(); foreach (const QString &key, keywords_.keys()) m_keywords.insert(key, keywords_.value(key).toString()); } modified(); } bool JobData::load(const QString &stateFilename) { if (!QFile::exists(stateFilename)) return false; QFile stateFile(stateFilename); if (!stateFile.open(QFile::ReadOnly | QFile::Text)) { Logger::logError(Logger::tr("Cannot read job information from %1.") .arg(stateFilename)); return false; } // Read file QByteArray inputText = stateFile.readAll(); stateFile.close(); // Parse JSON QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(inputText, &error); if (error.error != QJsonParseError::NoError) { Logger::logError(Logger::tr("Cannot parse job state from %1: %2\n%3") .arg(stateFilename) .arg(Logger::tr("%1 (at offset %2)") .arg(error.errorString()) .arg(error.offset)) .arg(inputText.data())); return false; } if (!doc.isObject()) { Logger::logError(Logger::tr("Error reading job state from %1: " "document is not an object!\n%2") .arg(stateFilename) .arg(inputText.data())); return false; } QJsonObject jobObject = doc.object(); if (!jobObject.contains("moleQueueId")) { Logger::logError(Logger::tr("Error reading job state from %1: " "No moleQueueId member!\n%2") .arg(stateFilename).arg(inputText.data())); return false; } setFromJson(jobObject); m_needsSync = false; return true; } bool JobData::save() { QString stateFilename = m_localWorkingDirectory + "/mqjobinfo.json"; QFile stateFile(stateFilename); if (!stateFile.open(QFile::ReadWrite | QFile::Text)) { Logger::logError(Logger::tr("Cannot save job information for job %1 in %2.") .arg(idTypeToString(moleQueueId())).arg(stateFilename), moleQueueId()); return false; } // Try to read existing data in QJsonDocument doc; QJsonParseError error; QJsonObject root; QByteArray inputText = stateFile.readAll(); if (!inputText.isEmpty()) { // Parse the file. doc = QJsonDocument::fromJson(inputText, &error); if (error.error != QJsonParseError::NoError) { Logger::logError(Logger::tr("Cannot parse existing state for job %1 in " "%2: %3. Job state not saved. File contents:" "\n%4") .arg(idTypeToString(moleQueueId())) .arg(stateFilename) .arg(Logger::tr("%1 (at offset %2)") .arg(error.errorString()) .arg(error.offset)) .arg(inputText.data()), moleQueueId()); stateFile.close(); return false; } // Verify that the JSON represents an object if (!doc.isObject()) { Logger::logError(Logger::tr("Internal error writing state for job %1 in %2:" " existing json root is not an object! Job " "state not saved.") .arg(idTypeToString(moleQueueId())).arg(stateFilename), moleQueueId()); stateFile.close(); return false; } } // Overlay the current job state onto the existing json: QJsonObject jobObject = toJsonObject(); foreach (const QString &key, jobObject.keys()) root.insert(key, jobObject.value(key)); // Write the data back out: QByteArray outputText = QJsonDocument(root).toJson(); stateFile.resize(0); stateFile.write(outputText); stateFile.close(); m_needsSync = false; return true; } } // end namespace MoleQueue molequeue-0.9.0/molequeue/app/jobdata.h000066400000000000000000000261451323436134600200740ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_JOBDATA_H #define MOLEQUEUE_JOBDATA_H #include "molequeueglobal.h" #include "filespecification.h" #include #include #include class ClientTest; class JobManagerTest; class ServerConnectionTest; namespace MoleQueue { class Client; class JobManager; class Server; /** * @class JobData jobdata.h * @brief Internal container for job details. * @author David C. Lonie * * Each JobData instance stores information about a specific job. All JobData * objects are owned by a JobManager, which dispenses JobReferenceBase * subclasses (Job and JobRequest) that are used to interact with the JobData * members. * */ class JobData { public: JobData(JobManager *parentManager); JobData(const JobData &); /// @return The parent JobManager JobManager *jobManager() const { return m_jobManager; } /// @param newQueue name of the queue. void setQueue(const QString &newQueue) { if (m_queue != newQueue) { m_queue = newQueue; modified(); } } /// @return Name of queue to use. QString queue() const { return m_queue; } /// @param newProgram Name of the program. void setProgram(const QString &newProgram) { if (m_program != newProgram) { m_program = newProgram; modified(); } } /// @return Name of program to run. QString program() const { return m_program; } /// @param state Status of job void setJobState(JobState state) { if (m_jobState != state) { m_jobState = state; modified(); } } /// @return Status of job JobState jobState() const { return m_jobState; } /// @param newDesc Description of job void setDescription(const QString &newDesc) { if (m_description != newDesc) { m_description = newDesc; modified(); } } /// @return newDesc Description of job QString description() const { return m_description; } /// @param filespec FileSpecification describing the main input file (called /// by the executable) void setInputFile(const FileSpecification &filespec) { m_inputFile = filespec; modified(); } /// @return FileSpecification describing the main input file (called by the /// executable) FileSpecification inputFile() const { return m_inputFile; } /// @param files FileSpecification objects describing additional input files /// to be placed in the working directory of the job prior to execution. void setAdditionalInputFiles(const QList & files) { m_additionalInputFiles = files; modified(); } /// @return FileSpecification objects describing additional input files to be /// placed in the working directory of the job prior to execution. QList additionalInputFiles() const { return m_additionalInputFiles; } /// @return A reference to the additional input files list. QList & additionalInputFilesRef() { return m_additionalInputFiles; } /// @param path String containing a location to copy the output files to after /// the job completes. Ignored if empty. void setOutputDirectory(const QString &path) { if (m_outputDirectory != path) { m_outputDirectory = path; modified(); } } /// @return String containing a location to copy the output files to after /// the job completes. Ignored if empty. QString outputDirectory() const { return m_outputDirectory; } /// @param path Temporary working directory where files are stored during job /// execution. void setLocalWorkingDirectory(const QString &path) { if (m_localWorkingDirectory != path) { m_localWorkingDirectory = path; modified(); } } /// @return Temporary working directory where files are stored during job /// execution. QString localWorkingDirectory() const { return m_localWorkingDirectory; } /// @param clean If true, delete any working files on the remote server. /// Default: false. void setCleanRemoteFiles(bool clean) { if (m_cleanRemoteFiles != clean) { m_cleanRemoteFiles = clean; modified(); } } /// @return If true, delete any working files on the remote server. /// Default: false. bool cleanRemoteFiles() const { return m_cleanRemoteFiles; } /// @param b If true, copies files back from remote server. Default: true void setRetrieveOutput(bool b) { if (m_retrieveOutput != b) { m_retrieveOutput = b; modified(); } } /// @return If true, copies files back from remote server. Default: true bool retrieveOutput() const { return m_retrieveOutput; } /// @param b If true, the local working files are removed after job is /// complete. Should be used with setOutputDirectory. Default: false void setCleanLocalWorkingDirectory(bool b) { if (m_cleanLocalWorkingDirectory != b) { m_cleanLocalWorkingDirectory = b; modified(); } } /// @return If true, the local working files are removed after job is /// complete. Should be used with setOutputDirectory. Default: false bool cleanLocalWorkingDirectory() const { return m_cleanLocalWorkingDirectory; } /// @param b If true, the job will not appear in the queue. Default: false void setHideFromGui(bool b) { if (m_hideFromGui != b) { m_hideFromGui = b; modified(); } } /// @return If true, the job will not appear in the queue. Default: false bool hideFromGui() const { return m_hideFromGui; } /// @param b If true, changes in the job state will trigger a popup /// notification from the MoleQueue system tray icon. Default: false void setPopupOnStateChange(bool b) { if (m_popupOnStateChange != b) { m_popupOnStateChange = b; modified(); } } /// @return If true, changes in the job state will trigger a popup /// notification from the MoleQueue system tray icon. Default: false bool popupOnStateChange() const { return m_popupOnStateChange; } /// @param num The total number of processor cores to use (if applicable). /// Default: 1 void setNumberOfCores(int num) { if (m_numberOfCores != num) { m_numberOfCores = num; modified(); } } /// @return The total number of processor cores to use (if applicable). /// Default: 1 int numberOfCores() const { return m_numberOfCores; } /// @param minutes The maximum walltime for this job in minutes. Setting this /// to a value <= 0 will use the queue-specific default max walltime. Only /// available for remote queues. Default is -1. void setMaxWallTime(int minutes) { if (m_maxWallTime != minutes) { m_maxWallTime = minutes; modified(); } } /// @return The maximum walltime for this job in minutes. Setting this to a /// value <= 0 will use the queue-specific default max walltime. Only /// available for remote queues. Default is -1. int maxWallTime() const { return m_maxWallTime; } /// @param id Internal MoleQueue identifier void setMoleQueueId(IdType id) { if (m_moleQueueId != id) { m_moleQueueId = id; modified(); } } /// @return Internal MoleQueue identifier IdType moleQueueId() const { return m_moleQueueId; } /// @param id Queue Job ID void setQueueId(IdType id) { if (m_queueId != id) { m_queueId = id; modified(); } } /// @return Queue Job ID IdType queueId() const { return m_queueId; } /// @return A reference to the job's keyword hash QHash & keywordsRef() { return m_keywords; } /// @param keyrep The keyword replacement hash void setKeywords(const QHash &keyrep) { if (m_keywords != keyrep) { m_keywords = keyrep; modified(); } } /// @return The keyword replacement hash QHash keywords() const { return m_keywords; } /// @return The Job's internal state as a QJsonObject QJsonObject toJsonObject() const; /// Update the Job's internal state from a QJsonObject void setFromJson(const QJsonObject &state); /// Initialize the JobData from the state in JSON file @a stateFileName bool load(const QString& stateFilename); /// Write a mqjobinfo.json file to the JobData's local working directory with /// the job state. bool save(); /// @return true if the JobData has changed since load() or save() was called. bool needsSync() const { return m_needsSync; } /// Called when the JobData is modified. void modified() { m_needsSync = true; } protected: /// Parent JobManager JobManager *m_jobManager; /// Name of queue to use QString m_queue; /// Name of program to run QString m_program; /// Current state of job JobState m_jobState; /// Description of job QString m_description; /// FileSpecification describing the main input file (called by the executable) FileSpecification m_inputFile; /// FileSpecification objects describing additional input files, to be placed /// in the working directory of the job prior to execution. QList m_additionalInputFiles; /// String containing a location to copy the output files to after the job /// completes. Ignored if empty. QString m_outputDirectory; /// Temporary working directory where files are stored during job execution. QString m_localWorkingDirectory; /// If true, delete any working files on the remote server. Default: false. bool m_cleanRemoteFiles; /// If true, copies files back from remote server. Default: true bool m_retrieveOutput; /// If true, the local working files are removed after job is complete. Should /// be used with setOutputDirectory. Default: false bool m_cleanLocalWorkingDirectory; /// If true, the job will not appear in the queue. Default: false bool m_hideFromGui; /// If true, changes in the job state will trigger a popup notification from /// the MoleQueue system tray icon. Default: true bool m_popupOnStateChange; /// The total number of processor cores to use (if applicable). /// Default: 1 int m_numberOfCores; /// The maximum walltime for this job in minutes. Setting this /// to a value <= 0 will use the queue-specific default max walltime. Only /// available for remote queues. Default is -1. int m_maxWallTime; /// Internal MoleQueue identifier IdType m_moleQueueId; /// Queue Job ID IdType m_queueId; /// List of custom keyword replacements for the job's launch script QHash m_keywords; /// True if the JobData has changed since load() or save() was called. bool m_needsSync; }; } // end namespace MoleQueue Q_DECLARE_METATYPE(MoleQueue::JobData*) Q_DECLARE_METATYPE(const MoleQueue::JobData*) #endif // JOB_H molequeue-0.9.0/molequeue/app/jobitemmodel.cpp000066400000000000000000000106611323436134600214710ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "jobitemmodel.h" #include "job.h" #include "jobmanager.h" #include namespace MoleQueue { JobItemModel::JobItemModel(QObject *parentObject) : QAbstractItemModel(parentObject), m_jobManager(NULL) { connect(this, SIGNAL(rowsInserted(QModelIndex, int, int)), this, SIGNAL(rowCountChanged())); connect(this, SIGNAL(rowsRemoved(QModelIndex, int, int)), this, SIGNAL(rowCountChanged())); connect(this, SIGNAL(modelReset()), this, SIGNAL(rowCountChanged())); connect(this, SIGNAL(layoutChanged()), this, SIGNAL(rowCountChanged())); } void JobItemModel::setJobManager(JobManager *newJobManager) { if (m_jobManager == newJobManager) return; beginResetModel(); if (m_jobManager) m_jobManager->disconnect(this); m_jobManager = newJobManager; connect(newJobManager, SIGNAL(jobUpdated(MoleQueue::Job)), this, SLOT(jobUpdated(MoleQueue::Job))); endResetModel(); } int JobItemModel::rowCount(const QModelIndex &modelIndex) const { if (m_jobManager && !modelIndex.isValid()) return m_jobManager->count(); else return 0; } int JobItemModel::columnCount(const QModelIndex &) const { return COLUMN_COUNT; } QVariant JobItemModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { switch (section) { case MOLEQUEUE_ID: return QVariant("#"); case JOB_TITLE: return QVariant("Job Title"); case NUM_CORES: return QVariant("Cores"); case QUEUE_NAME: return QVariant("Queue"); case PROGRAM_NAME: return QVariant("Program"); case JOB_STATE: return QVariant("Status"); default: return QVariant(); } } return QVariant(); } QVariant JobItemModel::data(const QModelIndex &modelIndex, int role) const { if (!m_jobManager || !modelIndex.isValid() || modelIndex.column() + 1 > COLUMN_COUNT) return QVariant(); Job job = m_jobManager->jobAt(modelIndex.row()); if (job.isValid()) { if (role == Qt::DisplayRole) { switch (modelIndex.column()) { case MOLEQUEUE_ID: return QVariant(job.moleQueueId()); case JOB_TITLE: return QVariant(job.description()); case NUM_CORES: return QVariant(job.numberOfCores()); case QUEUE_NAME: { if (job.queueId() != InvalidId) return QVariant(QString("%1 (%2)").arg(job.queue()) .arg(idTypeToString(job.queueId()))); else return QVariant(job.queue()); } case PROGRAM_NAME: return QVariant(job.program()); case JOB_STATE: return MoleQueue::jobStateToGuiString(job.jobState()); default: return QVariant(); } } else if (role == FetchJobRole) { return QVariant::fromValue(job); } } return QVariant(); } bool JobItemModel::removeRows(int row, int count, const QModelIndex &) { beginRemoveRows(QModelIndex(), row, row + count - 1); endRemoveRows(); return true; } bool JobItemModel::insertRows(int row, int count, const QModelIndex &) { beginInsertRows(QModelIndex(), row, row + count - 1); endInsertRows(); return true; } Qt::ItemFlags JobItemModel::flags(const QModelIndex &) const { return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } QModelIndex JobItemModel::index(int row, int column, const QModelIndex &/*modelIndex*/) const { if (m_jobManager && row >= 0 && row < m_jobManager->count()) return createIndex(row, column); else return QModelIndex(); } void JobItemModel::jobUpdated(const Job &job) { if (!m_jobManager) return; int row = m_jobManager->indexOf(job); if (row >= 0) emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); } } // End of namespace molequeue-0.9.0/molequeue/app/jobitemmodel.h000066400000000000000000000050071323436134600211340ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_JOBITEMMODEL_H #define MOLEQUEUE_JOBITEMMODEL_H #include #include "molequeueglobal.h" namespace MoleQueue { class Job; class JobManager; /// @brief Item model for interacting with jobs. class JobItemModel : public QAbstractItemModel { Q_OBJECT public: enum ColumnNames { MOLEQUEUE_ID = 0, JOB_TITLE , NUM_CORES, QUEUE_NAME, PROGRAM_NAME, JOB_STATE, COLUMN_COUNT // Use to get the total number of columns }; explicit JobItemModel(QObject *parentObject = 0); // Used with the data() method to get info. enum UserRoles { FetchJobRole = Qt::UserRole }; void setJobManager(JobManager *jobManager); JobManager *jobManager() const {return m_jobManager;} QModelIndex parent(const QModelIndex &) const {return QModelIndex();} int rowCount(const QModelIndex & theModelIndex = QModelIndex()) const; int columnCount(const QModelIndex & modelIndex = QModelIndex()) const; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; QVariant data(const QModelIndex & modelIndex, int role = Qt::DisplayRole) const; /// Remove the rows from the model. Does not modify the underlying data /// structure. /// @see JobManager::removeJob() bool removeRows(int row, int count, const QModelIndex &); /// Insert rows into the model. Does not modify the underlying data structure. /// @see JobManager::newJob() bool insertRows(int row, int count, const QModelIndex &); Qt::ItemFlags flags(const QModelIndex & modelIndex) const; QModelIndex index(int row, int column, const QModelIndex & modelIndex = QModelIndex()) const; friend class MoleQueue::JobManager; signals: void rowCountChanged(); public slots: void jobUpdated(const MoleQueue::Job &job); protected: JobManager *m_jobManager; }; } // End namespace #endif molequeue-0.9.0/molequeue/app/jobmanager.cpp000066400000000000000000000135641323436134600211310ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "jobmanager.h" #include "job.h" #include "jobdata.h" #include "jobitemmodel.h" #include "logger.h" #include #include namespace MoleQueue { JobManager::JobManager(QObject *parentObject) : QObject(parentObject), m_itemModel(new JobItemModel(this)) { qRegisterMetaType("MoleQueue::Job"); m_itemModel->setJobManager(this); connect(this, SIGNAL(jobStateChanged(MoleQueue::Job,MoleQueue::JobState, MoleQueue::JobState)), this, SIGNAL(jobUpdated(MoleQueue::Job))); } JobManager::~JobManager() { m_moleQueueMap.clear(); qDeleteAll(m_jobs); m_jobs.clear(); } void JobManager::loadJobState(const QString &path) { m_itemModel->beginResetModel(); QDir dir(path); foreach (const QString &subDirName, dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot)) { QString stateFilename(QDir::cleanPath(dir.absolutePath() + "/" + subDirName + "/mqjobinfo.json")); if (!QFile::exists(stateFilename)) continue; JobData *jobdata = new JobData(this); if (jobdata->load(stateFilename)) { m_jobs.append(jobdata); insertJobData(jobdata); } else { delete jobdata; } } m_itemModel->endResetModel(); } void JobManager::syncJobState() const { foreach (JobData *jobdata, m_jobs) { if (jobdata->needsSync()) jobdata->save(); } } Job JobManager::newJob() { JobData *jobdata = new JobData(this); m_jobs.append(jobdata); emit jobAboutToBeAdded(Job(jobdata)); insertJobData(jobdata); syncJobState(); return Job(jobdata); } Job JobManager::newJob(const QJsonObject &jobState) { JobData *jobdata = new JobData(this); jobdata->setFromJson(jobState); jobdata->setMoleQueueId(InvalidId); m_jobs.append(jobdata); emit jobAboutToBeAdded(Job(jobdata)); insertJobData(jobdata); syncJobState(); return Job(jobdata); } void JobManager::removeJob(JobData *jobdata) { if (!jobdata || !m_jobs.contains(jobdata)) return; emit jobAboutToBeRemoved(Job(jobdata)); IdType moleQueueId = jobdata->moleQueueId(); int jobsIndex = m_jobs.indexOf(jobdata); m_jobs.removeAt(jobsIndex); m_itemModel->removeRow(jobsIndex); m_moleQueueMap.remove(moleQueueId); // Save job state and move it so it won't get loaded next time. jobdata->save(); QFile::rename(jobdata->localWorkingDirectory() + "/mqjobinfo.json", jobdata->localWorkingDirectory() + "/mqjobinfo-archived.json"); delete jobdata; emit jobRemoved(moleQueueId); } void JobManager::removeJob(IdType moleQueueId) { JobData *jobdata = lookupJobDataByMoleQueueId(moleQueueId); if (jobdata) removeJob(jobdata); } void JobManager::removeJob(const Job &job) { if (job.isValid()) removeJob(job.jobData()); } void JobManager::removeJobs(const QList &jobsToRemove) { foreach (const Job &job, jobsToRemove) removeJob(job); } void JobManager::removeJobs(const QList &moleQueueIds) { foreach(IdType moleQueueId, moleQueueIds) removeJob(moleQueueId); } Job JobManager::lookupJobByMoleQueueId(IdType moleQueueId) const { return Job(lookupJobDataByMoleQueueId(moleQueueId)); } QList JobManager::jobsWithJobState(JobState state) { QList result; foreach (JobData *jobdata, m_jobs) { if (jobdata->jobState() == state) result << Job(jobdata); } return result; } Job JobManager::jobAt(int i) const { if (Q_LIKELY(i >= 0 && i < m_jobs.size())) return Job(m_jobs.at(i)); return Job(); } int JobManager::indexOf(const Job &job) const { JobData *jobdata = job.jobData(); if (jobdata) return m_jobs.indexOf(jobdata); return -1; } void JobManager::moleQueueIdChanged(const Job &job) { JobData *jobdata = job.jobData(); if (!m_jobs.contains(jobdata)) return; if (lookupJobDataByMoleQueueId(jobdata->moleQueueId()) != jobdata) { IdType oldMoleQueueId = m_moleQueueMap.key(jobdata, InvalidId); if (oldMoleQueueId != InvalidId) m_moleQueueMap.remove(oldMoleQueueId); m_moleQueueMap.insert(jobdata->moleQueueId(), jobdata); } } void JobManager::setJobState(IdType moleQueueId, JobState newState) { JobData *jobdata = lookupJobDataByMoleQueueId(moleQueueId); if (!jobdata) return; const JobState oldState = jobdata->jobState(); if (oldState == newState) return; jobdata->setJobState(newState); Logger::logNotification(tr("Job '%1' has changed status from '%2' to '%3'.") .arg(jobdata->description()) .arg(MoleQueue::jobStateToGuiString(oldState)) .arg(MoleQueue::jobStateToGuiString(newState)), moleQueueId); emit jobStateChanged(jobdata, oldState, newState); } void JobManager::setJobQueueId(IdType moleQueueId, IdType queueId) { JobData *jobdata = lookupJobDataByMoleQueueId(moleQueueId); if (!jobdata) return; if (jobdata->queueId() == queueId) return; jobdata->setQueueId(queueId); emit jobUpdated(jobdata); } void JobManager::insertJobData(JobData *jobdata) { if (jobdata->moleQueueId() != MoleQueue::InvalidId) m_moleQueueMap.insert(jobdata->moleQueueId(), jobdata); m_itemModel->insertRow(m_jobs.size() - 1); emit jobAdded(Job(jobdata)); } } // end namespace MoleQueue molequeue-0.9.0/molequeue/app/jobmanager.h000066400000000000000000000154301323436134600205700ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef JOBMANAGER_H #define JOBMANAGER_H #include #include "job.h" #include class QJsonObject; class ConnectionTest; namespace MoleQueue { class JobData; class JobItemModel; class JobReferenceBase; /** * @class JobManager jobmanager.h * @brief Owns and manages JobData objects. * @author David C. Lonie * * The JobManager class owns all JobData objects. At least two JobManager * objects exist during normal operation; the Client class holds a JobManager to * track all jobs belonging to that client, and the Server class of the * MoleQueue server holds a JobManager to track all jobs that it is managing. */ class JobManager : public QObject { Q_OBJECT public: explicit JobManager(QObject *parentObject = 0); virtual ~JobManager(); /// Locate jobs from the subdirectories in @a path. This function will look /// in all immediate subdirectories of @a path and load mqjobinfo.json files /// with JobData::load(). void loadJobState(const QString &path); /// Sync all job state with disk. void syncJobState() const; /** * @name Job Management * Functions to add, remove, or locate jobs. * @{ */ /** * @return Insert a new JobData object into the JobManager's jobMap. The * JobData is set to default values and a Job reference to it is returned. */ Job newJob(); /** * @param jobState A QJsonObject describing the state of the new Job. * @return A new Job object, initialized to the state in @a jobState. * @sa Job::toJsonObject() Job::setFromJson() */ Job newJob(const QJsonObject &jobState); /** * Remove the specified @a jobdata from this manager and delete it. All Job * objects with @a job's MoleQueue id will be invalidated. */ void removeJob(JobData* jobdata); /** * Remove the job with the specified @a moleQueueId from this manager and * delete it. */ void removeJob(IdType moleQueueId); /** * Remove the specified @a job from this manager and delete it. All Job * objects with @a job's MoleQueue id will be invalidated. */ void removeJob(const Job &job); /** * Remove the specified @a jobs from this manager and delete them. */ void removeJobs(const QList &jobsToRemove); /** * Remove the jobs with the specified @a moleQueueIds from this manager and * delete them. */ void removeJobs(const QList &moleQueueIds); /** * @param moleQueueId The MoleQueue Id of the requested Job. * @return The Job with the requested MoleQueue Id. * @note If no such Job exists, Job::isValid() will return false; */ Job lookupJobByMoleQueueId(IdType moleQueueId) const; /** * Return a list of Job objects that have JobState @a state. * @param state JobState of interests * @return List of Job objects with JobState @a state */ QList jobsWithJobState(MoleQueue::JobState state); /** * @return Number of Job objects held by this manager. */ int count() const { return m_jobs.size(); } /** * Index based job look up. Use with count() to iterate over all Jobs in the * manager. Jobs are not sorted in any particular order. * @return The job with index @a i */ Job jobAt(int i) const; /** * Lookup iteratible index of Job &job. Compatible with count() and jobAt(). * @return index of @a job, or -1 if @a job is invalid. */ int indexOf(const Job &job) const; /** * @return the JobItemModel for this JobManager. */ JobItemModel * itemModel() const { return m_itemModel; } friend class JobReferenceBase; friend class ConnectionTest; public slots: /** * Inform the QueueManager that the MoleQueue id of @a job has * changed so that it may update its internal lookup tables. * @param job The Job object. */ void moleQueueIdChanged(const MoleQueue::Job &job); // End Job Management group: /** * @} */ /** * @name Job Modification * Methods to change properties of jobs. * @{ */ /** * Set the JobState for the job with the specified MoleQueue id */ void setJobState(MoleQueue::IdType jobManagerId, MoleQueue::JobState newState); /** * Set the QueueId for the job with the specified MoleQueue id */ void setJobQueueId(MoleQueue::IdType jobManagerIdId, MoleQueue::IdType queueId); // End Job Modification group /** * @} */ signals: /** * Emitted when a job is about to be inserted. Client and MainWindow should * directly connect slots to this signal which will set the molequeue id and * local working directory. */ void jobAboutToBeAdded(MoleQueue::Job job); /** * Emitted when a Job has been added to this JobManager. * @param job The new Job object. */ void jobAdded(const MoleQueue::Job &job); /** * Emitted when a Job changes JobState. * @param job Job object * @param oldState Previous state of @a job * @param newState New state of @a job */ void jobStateChanged(const MoleQueue::Job &job, MoleQueue::JobState oldState, MoleQueue::JobState newState); /** * Emitted when a Job's state changes. * @param job * @param queueId */ void jobUpdated(const MoleQueue::Job &job); /** * Emitted when the @a job is about to be removed and deleted. */ void jobAboutToBeRemoved(const MoleQueue::Job &job); /** * Emitted when the job with the specified @a moleQueueId has been removed * and deleted. */ void jobRemoved(MoleQueue::IdType moleQueueId); protected: /// @return The JobData with @a moleQueueId JobData *lookupJobDataByMoleQueueId(IdType moleQueueId) const { return m_moleQueueMap.value(moleQueueId, NULL); } /// @return Whether the address @a data is stored in m_jobs. bool hasJobData(const JobData *data) const { return m_jobs.contains(const_cast(data)); } /// @param jobdata Job to insert into the internal lookup structures. void insertJobData(JobData *jobdata); /// "Master" list of JobData QList m_jobs; /// Item model for interacting with jobs JobItemModel *m_itemModel; /// Lookup table for MoleQueue ids QMap m_moleQueueMap; }; } #endif // JOBMANAGER_H molequeue-0.9.0/molequeue/app/jobreferencebase.cpp000066400000000000000000000045451323436134600223070ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "jobreferencebase.h" #include "jobdata.h" #include "jobmanager.h" namespace MoleQueue { JobReferenceBase::JobReferenceBase(JobData *jobdata) : m_jobData(jobdata), m_jobManager( Q_LIKELY(jobdata != NULL) ? jobdata->jobManager() : NULL), m_moleQueueId( Q_LIKELY(jobdata != NULL) ? jobdata->moleQueueId() : MoleQueue::InvalidId ) { } JobReferenceBase::JobReferenceBase(JobManager *jobManager, IdType moleQueueId) : m_jobData(jobManager->lookupJobDataByMoleQueueId(moleQueueId)), m_jobManager(jobManager), m_moleQueueId(moleQueueId) { } JobReferenceBase::~JobReferenceBase() { } bool JobReferenceBase::isValid() const { if (m_jobData) { // If we have a molequeue id, validate the job data using the faster QMap // lookup, O(log(n)) if (m_moleQueueId != InvalidId) { JobData *ref = m_jobManager->lookupJobDataByMoleQueueId(m_moleQueueId); if (ref) { if (ref == m_jobData) return true; qWarning() << "Job with molequeue id" << m_moleQueueId << "maps to a " "different job than expected.\nExpected:\n" << m_jobData << "\nLookup returned:\n" << ref; return false; } } // If the cached molequeue id is invalid, do the slow QList lookup, O(n): if (m_jobManager->hasJobData(m_jobData)) { // m_jobData is still valid. Try to update our cached molequeueid: if (m_jobData->moleQueueId() != InvalidId) m_moleQueueId = m_jobData->moleQueueId(); return true; } // m_jobData is gone... m_jobData = NULL; return false; } // If m_jobData is NULL, the reference is invalid return false; } } // namespace MoleQueue molequeue-0.9.0/molequeue/app/jobreferencebase.h000066400000000000000000000067631323436134600217600ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_JOBREFERENCEBASE_H #define MOLEQUEUE_JOBREFERENCEBASE_H #include "molequeueglobal.h" #include "idtypeutils.h" #include namespace MoleQueue { class JobData; class JobManager; /** * @class JobReferenceBase jobreferencebase.h * @brief Base class for lightweight interfaces to JobData objects. * @author David C. Lonie * * JobData objects, owned by JobManager, each contain data pertaining to a * specific job running a Program on a Queue. JobData contains several dynamic * properties that change during it's lifetime, e.g. Queue id and JobState. To * avoid having out-of-date references in the MoleQueue application, subclasses * of JobReferenceBase provide a convenient interface for obtaining and * modifying job properties. * * JobReferenceBase validates the pointer to the JobData object it represents by * querying the JobManager. The validity of the JobData pointer can be checked * with isValid(), which will return false if the JobData has been removed from * the JobManager. Subclasses of JobReferenceBase, Job on the Server and * JobRequest on the Client, will forward requests to the JobData. Certain * methods may cause signals to be emitted from JobManager; these cases will be * noted in the method documentation. */ class JobReferenceBase { public: /// Construct a new JobReferenceBase with the specified JobData explicit JobReferenceBase(JobData *jobdata = NULL); /// Construct a new JobReferenceBase for the job with the MoleQueueId /// in the indicated JobManager JobReferenceBase(JobManager *jobManager, IdType moleQueueId); /// Construct a new JobReferenceBase with the same JobData as @a other. JobReferenceBase(const JobReferenceBase &other) : m_jobData(other.m_jobData),m_jobManager(other.m_jobManager), m_moleQueueId(other.m_moleQueueId) {} virtual ~JobReferenceBase(); /// Returns true if both JobReferenceBases are valid and refer to the same /// JobData instance. bool operator==(const JobReferenceBase &other) const { return isValid() && other.isValid() && m_jobData == other.m_jobData; } /// @return true if the guarded JobData pointer is valid, false otherwise. bool isValid() const; friend class JobManager; protected: JobData * jobData() const { return m_jobData; } /// Print a warning with debugging info and return false if isValid() returns /// false. bool warnIfInvalid() const { if (Q_LIKELY(isValid())) return true; qWarning() << "Invalid reference to job with MoleQueue id " << idTypeToString(m_moleQueueId) << " accessed!"; return false; } /// May be set to NULL during validation mutable JobData* m_jobData; JobManager *m_jobManager; /// Used to speed up lookups and validation mutable IdType m_moleQueueId; }; } // namespace MoleQueue #endif // MOLEQUEUE_JOBREFERENCEBASE_H molequeue-0.9.0/molequeue/app/jobtableproxymodel.cpp000066400000000000000000000151551323436134600227270ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "jobtableproxymodel.h" #include "jobitemmodel.h" #include "job.h" #include namespace MoleQueue { JobTableProxyModel::JobTableProxyModel(QObject *parent_) : QSortFilterProxyModel(parent_) { connect(this, SIGNAL(rowsInserted(QModelIndex, int, int)), this, SIGNAL(rowCountChanged())); connect(this, SIGNAL(rowsRemoved(QModelIndex, int, int)), this, SIGNAL(rowCountChanged())); connect(this, SIGNAL(modelReset()), this, SIGNAL(rowCountChanged())); connect(this, SIGNAL(layoutChanged()), this, SIGNAL(rowCountChanged())); QSettings settings; settings.beginGroup("jobTable"); settings.beginGroup("filter"); m_filterString = settings.value("filterString").toString(); m_showHiddenJobs = settings.value("showHidden", true).toBool(); settings.beginGroup("status"); m_showStatusNew = settings.value("new", true).toBool(); m_showStatusSubmitted = settings.value("submitted", true).toBool(); m_showStatusQueued = settings.value("queued", true).toBool(); m_showStatusRunning = settings.value("running", true).toBool(); m_showStatusFinished = settings.value("finished", true).toBool(); m_showStatusCanceled = settings.value("canceled", true).toBool(); m_showStatusError = settings.value("error", true).toBool(); settings.endGroup(); // status settings.endGroup(); // filter settings.endGroup(); // jobTable } JobTableProxyModel::~JobTableProxyModel() { saveState(); } void JobTableProxyModel::setFilterString(const QString &str) { if (m_filterString == str) return; m_filterString = str; saveState(); invalidateFilter(); } void JobTableProxyModel::setShowStatusNew(bool show) { if (m_showStatusNew == show) return; m_showStatusNew = show; saveState(); invalidateFilter(); } void JobTableProxyModel::setShowStatusSubmitted(bool show) { if (m_showStatusSubmitted == show) return; m_showStatusSubmitted = show; saveState(); invalidateFilter(); } void JobTableProxyModel::setShowStatusQueued(bool show) { if (m_showStatusQueued == show) return; m_showStatusQueued = show; saveState(); invalidateFilter(); } void JobTableProxyModel::setShowStatusRunning(bool show) { if (m_showStatusRunning == show) return; m_showStatusRunning = show; saveState(); invalidateFilter(); } void JobTableProxyModel::setShowStatusFinished(bool show) { if (m_showStatusFinished == show) return; m_showStatusFinished = show; saveState(); invalidateFilter(); } void JobTableProxyModel::setShowStatusCanceled(bool show) { if (m_showStatusCanceled == show) return; m_showStatusCanceled = show; saveState(); invalidateFilter(); } void JobTableProxyModel::setShowStatusError(bool show) { if (m_showStatusError == show) return; m_showStatusError = show; saveState(); invalidateFilter(); } void JobTableProxyModel::setShowHiddenJobs(bool show) { if (m_showHiddenJobs == show) return; m_showHiddenJobs = show; saveState(); invalidateFilter(); } bool JobTableProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { QModelIndex sourceIndex = sourceModel()->index(sourceRow, 0, sourceParent); Job job = static_cast(sourceModel())->data( sourceIndex, JobItemModel::FetchJobRole).value(); if (!job.isValid()) return false; if (job.hideFromGui() && !m_showHiddenJobs) return false; switch (job.jobState()) { case Unknown: case None: case Accepted: if (!m_showStatusNew) return false; break; case QueuedLocal: case QueuedRemote: if (!m_showStatusQueued) return false; break; case Submitted: if (!m_showStatusSubmitted) return false; break; case RunningLocal: case RunningRemote: if (!m_showStatusRunning) return false; break; case Finished: if (!m_showStatusFinished) return false; break; case Canceled: if (!m_showStatusCanceled) return false; break; case Error: if (!m_showStatusError) return false; break; default: break; } if (!m_filterString.isEmpty()) { QStringList filterTerms = m_filterString.split(QRegExp("\\s+"), QString::SkipEmptyParts); foreach (QString fullTerm, filterTerms) { bool termMatch = false; bool isNegated = false; QStringRef term(&fullTerm); // terms starting with '-' should not be present if (term.startsWith('-')) { isNegated = true; term = fullTerm.midRef(1); } for (int i = 0; i < static_cast(sourceModel()->columnCount()); ++i) { const QVariant disp = sourceModel()->data( sourceModel()->index(sourceRow, i), Qt::DisplayRole); if (disp.canConvert(QVariant::String)) { if (disp.toString().contains(term, Qt::CaseInsensitive)) { termMatch = true; break; } // end if string matches } // end if variant is string } // end foreach column // If the term matches in a negated search or vice-versa, the row is // not shown if (termMatch == isNegated) return false; } // end foreach term } // end if filter string exists return true; } void JobTableProxyModel::saveState() const { QSettings settings; settings.beginGroup("jobTable"); settings.beginGroup("filter"); settings.setValue("filterString", m_filterString); settings.setValue("showHidden", m_showHiddenJobs); settings.beginGroup("status"); settings.setValue("new", m_showStatusNew); settings.setValue("submitted", m_showStatusSubmitted); settings.setValue("queued", m_showStatusQueued); settings.setValue("running", m_showStatusRunning); settings.setValue("finished", m_showStatusFinished); settings.setValue("canceled", m_showStatusCanceled); settings.setValue("error", m_showStatusError); settings.endGroup(); // status settings.endGroup(); // filter settings.endGroup(); // jobTable } } // namespace MoleQueue molequeue-0.9.0/molequeue/app/jobtableproxymodel.h000066400000000000000000000045741323436134600223770ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_JOBTABLEPROXYMODEL_H #define MOLEQUEUE_JOBTABLEPROXYMODEL_H #include namespace MoleQueue { /// @brief Filtering item model for the JobTableWidget job list. class JobTableProxyModel : public QSortFilterProxyModel { Q_OBJECT public: explicit JobTableProxyModel(QObject *parent_ = 0); ~JobTableProxyModel(); QString filterString() const { return m_filterString; } bool showStatusNew() const { return m_showStatusNew; } bool showStatusSubmitted() const { return m_showStatusSubmitted; } bool showStatusQueued() const { return m_showStatusQueued; } bool showStatusRunning() const { return m_showStatusRunning; } bool showStatusFinished() const { return m_showStatusFinished; } bool showStatusCanceled() const { return m_showStatusCanceled; } bool showStatusError() const { return m_showStatusError; } bool showHiddenJobs() const { return m_showHiddenJobs; } signals: void rowCountChanged(); public slots: void setFilterString(const QString &str); void setShowStatusNew(bool show); void setShowStatusSubmitted(bool show); void setShowStatusQueued(bool show); void setShowStatusRunning(bool show); void setShowStatusFinished(bool show); void setShowStatusCanceled(bool show); void setShowStatusError(bool show); void setShowHiddenJobs(bool show); protected: bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const; void saveState() const; private: QString m_filterString; bool m_showStatusNew; bool m_showStatusSubmitted; bool m_showStatusQueued; bool m_showStatusRunning; bool m_showStatusFinished; bool m_showStatusCanceled; bool m_showStatusError; bool m_showHiddenJobs; }; } // namespace MoleQueue #endif // MOLEQUEUE_JOBTABLEPROXYMODEL_H molequeue-0.9.0/molequeue/app/jobtablewidget.cpp000066400000000000000000000077141323436134600220120ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "jobtablewidget.h" #include "ui_jobtablewidget.h" #include "advancedfilterdialog.h" #include "jobmanager.h" #include "jobitemmodel.h" #include "jobtableproxymodel.h" #include namespace MoleQueue { JobTableWidget::JobTableWidget(QWidget *parentObject) : QWidget(parentObject), ui(new Ui::JobTableWidget), m_jobManager(NULL), m_proxyModel(new JobTableProxyModel (this)), m_filterDialog(NULL) { ui->setupUi(this); connect(m_proxyModel, SIGNAL(rowCountChanged()), this, SLOT(modelRowCountChanged())); ui->table->setModel(m_proxyModel); ui->table->setSortingEnabled(true); connect(ui->filterEdit, SIGNAL(textChanged(QString)), this, SLOT(updateFilters())); connect(ui->filterMore, SIGNAL(clicked()), this, SLOT(showAdvancedFilterDialog())); ui->filterEdit->setText(m_proxyModel->filterString()); } JobTableWidget::~JobTableWidget() { delete ui; } void JobTableWidget::setJobManager(MoleQueue::JobManager *jobMan) { if (jobMan == m_jobManager) return; if (m_jobManager) { disconnect(m_jobManager->itemModel(), SIGNAL(rowCountChanged()), this, SLOT(modelRowCountChanged())); } m_jobManager = jobMan; connect(m_jobManager->itemModel(), SIGNAL(rowCountChanged()), this, SLOT(modelRowCountChanged())); m_proxyModel->setSourceModel(jobMan->itemModel()); m_proxyModel->setDynamicSortFilter(true); for (int i = 0; i < m_proxyModel->columnCount(); ++i) { if (i == JobItemModel::JOB_TITLE) { // stretch description ui->table->horizontalHeader() ->setSectionResizeMode(i, QHeaderView::Stretch); } else { // resize to fit others ui->table->horizontalHeader() ->setSectionResizeMode(i, QHeaderView::ResizeToContents); } } modelRowCountChanged(); } void JobTableWidget::clearFinishedJobs() { if (!m_jobManager) return; QList finishedJobs = m_jobManager->jobsWithJobState(MoleQueue::Finished); finishedJobs.append(m_jobManager->jobsWithJobState(MoleQueue::Canceled)); QMessageBox::StandardButton confirm = QMessageBox::question(this, tr("Really remove jobs?"), tr("Are you sure you would like to remove %n " "finished job(s)? This will not delete any input" " or output files.", "", finishedJobs.size()), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (confirm != QMessageBox::Yes) return; m_jobManager->removeJobs(finishedJobs); } void JobTableWidget::showFilterBar(bool visible) { if (visible) focusInFilter(); else ui->filterBar->hide(); } void JobTableWidget::focusInFilter() { if (!ui->filterBar->isVisible()) ui->filterBar->show(); ui->filterEdit->setFocus(); ui->filterEdit->selectAll(); } void JobTableWidget::showAdvancedFilterDialog() { if (m_filterDialog == NULL) { m_filterDialog = new AdvancedFilterDialog(m_proxyModel, this); } m_filterDialog->show(); m_filterDialog->raise(); } void JobTableWidget::updateFilters() { m_proxyModel->setFilterString(ui->filterEdit->text()); } void JobTableWidget::modelRowCountChanged() { if (m_jobManager) emit jobCountsChanged(m_jobManager->itemModel()->rowCount(), m_proxyModel->rowCount()); } } // end namespace MoleQueue molequeue-0.9.0/molequeue/app/jobtablewidget.h000066400000000000000000000034471323436134600214560ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_JOBTABLEWIDGET_H #define MOLEQUEUE_JOBTABLEWIDGET_H #include namespace Ui { class JobTableWidget; } namespace MoleQueue { class AdvancedFilterDialog; class Job; class JobActionFactory; class JobManager; class JobTableProxyModel; /// @brief Widget which encapsulates the Job table MVC classes. class JobTableWidget : public QWidget { Q_OBJECT public: explicit JobTableWidget(QWidget *parentObject = 0); ~JobTableWidget(); void setJobManager(JobManager *jobManager); JobManager * jobManager() const { return m_jobManager; } signals: void jobCountsChanged(int totalJobs, int shownJobs); public slots: void clearFinishedJobs(); void showFilterBar(bool visible = true); void hideFilterBar() { showFilterBar(false); } void focusInFilter(); void showAdvancedFilterDialog(); protected slots: void updateFilters(); void modelRowCountChanged(); protected: // Row indices, ascending order QList getSelectedRows(); Ui::JobTableWidget *ui; JobManager *m_jobManager; JobTableProxyModel *m_proxyModel; AdvancedFilterDialog *m_filterDialog; }; } // end namespace MoleQueue #endif // MOLEQUEUE_JOBTABLEWIDGET_H molequeue-0.9.0/molequeue/app/jobview.cpp000066400000000000000000000065341323436134600204700ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "jobview.h" #include "actionfactorymanager.h" #include "job.h" #include "jobactionfactory.h" #include "jobitemmodel.h" #include "jobtableproxymodel.h" #include #include #include #include #include #include #include namespace MoleQueue { JobView::JobView(QWidget *theParent) : QTableView(theParent) { } JobView::~JobView() { } void JobView::contextMenuEvent(QContextMenuEvent *e) { // list of action factories. Map to sort by usefulness QMap factoryMap; ActionFactoryManager *manager = ActionFactoryManager::instance(); foreach (JobActionFactory *factory, manager->factories(JobActionFactory::ContextItem)) { factoryMap.insertMulti(factory->usefulness(), factory); } // Get job under cursor Job cursorJob = model()->data(indexAt(e->pos()), JobItemModel::FetchJobRole).value(); // Get selected jobs QList jobs = selectedJobs(); QMenu *menu = new QMenu(this); // Factories sorted by usefulness: QList factories = factoryMap.values(); foreach (JobActionFactory *factory, factories) { factory->clearJobs(); // Add all selected jobs if the factory is multijob. Otherwise just the one // under the cursor. if (factory->isMultiJob()) { foreach (const Job &job, jobs) factory->addJobIfValid(job); } else { factory->addJobIfValid(cursorJob); } if (factory->hasValidActions()) { if (menu->actions().size()) menu->addSeparator(); // Call createActions before menuText, as menuText isn't always static. QList someActions = factory->createActions(); QMenu *actionMenu = menu; if (factory->useMenu()) actionMenu = menu->addMenu(factory->menuText()); foreach (QAction *action, someActions) { actionMenu->addAction(action); action->setParent(actionMenu); } } } menu->exec(QCursor::pos()); } QList JobView::selectedJobs() { QList result; JobTableProxyModel *proxyModel = qobject_cast(model()); if (!proxyModel) return result; JobItemModel *sourceModel = qobject_cast(proxyModel->sourceModel()); if (!sourceModel) return result; QModelIndexList proxySelection = selectionModel()->selectedRows(); foreach (const QModelIndex &ind, proxySelection) { Job job = sourceModel->data(proxyModel->mapToSource(ind), JobItemModel::FetchJobRole).value(); if (job.isValid()) result << job; } return result; } } // End of namespace molequeue-0.9.0/molequeue/app/jobview.h000066400000000000000000000020541323436134600201260ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_JOBVIEW_H #define MOLEQUEUE_JOBVIEW_H #include namespace MoleQueue { class Job; /// MVC item view for the job table. class JobView : public QTableView { Q_OBJECT public: JobView(QWidget *theParent = 0); ~JobView(); /** Custom context menu for this view. */ void contextMenuEvent(QContextMenuEvent *e); QList selectedJobs(); }; } // End of namespace #endif molequeue-0.9.0/molequeue/app/localqueuewidget.cpp000066400000000000000000000025141323436134600223600ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "localqueuewidget.h" #include "ui_localqueuewidget.h" #include "queues/local.h" namespace MoleQueue { LocalQueueWidget::LocalQueueWidget(QueueLocal *queue, QWidget *parent_) : AbstractQueueSettingsWidget(parent_), ui(new Ui::LocalQueueWidget), m_queue(queue) { ui->setupUi(this); reset(); connect(ui->coresSpinBox, SIGNAL(valueChanged(int)), this, SLOT(setDirty())); } LocalQueueWidget::~LocalQueueWidget() { delete ui; } void LocalQueueWidget::save() { m_queue->setMaxNumberOfCores(ui->coresSpinBox->value()); setDirty(false); } void LocalQueueWidget::reset() { ui->coresSpinBox->setValue(m_queue->maxNumberOfCores()); setDirty(false); } } // namespace MoleQueue molequeue-0.9.0/molequeue/app/localqueuewidget.h000066400000000000000000000023211323436134600220210ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_LOCALQUEUEWIDGET_H #define MOLEQUEUE_LOCALQUEUEWIDGET_H #include "abstractqueuesettingswidget.h" namespace Ui { class LocalQueueWidget; } namespace MoleQueue { class QueueLocal; /// @brief Configuration widget for local queues. class LocalQueueWidget : public AbstractQueueSettingsWidget { Q_OBJECT public: LocalQueueWidget(QueueLocal *queue, QWidget *parent_ = 0); ~LocalQueueWidget(); public slots: void save(); void reset(); private: Ui::LocalQueueWidget *ui; QueueLocal *m_queue; }; } // namespace MoleQueue #endif // MOLEQUEUE_LOCALQUEUEWIDGET_H molequeue-0.9.0/molequeue/app/logentry.cpp000066400000000000000000000041751323436134600206650ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "logentry.h" #include "idtypeutils.h" #include namespace MoleQueue { LogEntry::LogEntry(LogEntryType type, const QString &message_, const IdType &moleQueueId_) : m_message(message_), m_moleQueueId(moleQueueId_), m_entryType(type) { } LogEntry::LogEntry(const QJsonObject &json) : m_message(json.value("message").isString() ? json.value("message").toString() : QString("Invalid JSON!")), m_moleQueueId(toIdType(json.value("moleQueueId"))), m_entryType(json.value("entryType").isDouble() ? static_cast( static_cast(json.value("entryType").toDouble() + 0.5)) : Error), m_timeStamp(json.value("time").isString() ? QDateTime::fromString(json.value("time").toString()) : QDateTime()) { } LogEntry::LogEntry(const LogEntry &other) : m_message(other.m_message), m_moleQueueId(other.m_moleQueueId), m_entryType(other.m_entryType), m_timeStamp(other.m_timeStamp) { } LogEntry::~LogEntry() { } void LogEntry::writeSettings(QJsonObject &root) const { root.insert("message", m_message); root.insert("moleQueueId", idTypeToJson(m_moleQueueId)); root.insert("entryType", static_cast(m_entryType)); root.insert("time", m_timeStamp.toString()); } void LogEntry::setTimeStamp() { m_timeStamp = QDateTime::currentDateTime(); } const QDateTime &LogEntry::timeStamp() const { return m_timeStamp; } } // namespace MoleQueue molequeue-0.9.0/molequeue/app/logentry.h000066400000000000000000000104641323436134600203300ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_LOGENTRY_H #define MOLEQUEUE_LOGENTRY_H #include "molequeueglobal.h" #include #include class QJsonObject; namespace MoleQueue { class Logger; /** * @class LogEntry logentry.h * @brief Message and metadata for log messages. * @author David C. Lonie * * Each LogEntry object represents an entry in the MoleQueue log. LogEntries * fall into one of four categories: * - DebugMessage: Verbose debugging information. * - Notification: Routine information that is relevant to the user. * - Warning: Non-routine information that is relevant to the user, but does not * indicate a serious problem. * - Error: Serious problem that will affect either the MoleQueue application * or a Job's ability to perform properly. * * The easiest way to add new entries to the log is to use the static functions * in Logger: * - Logger::logDebugMessage(QString message, IdType moleQueueId) * - Logger::logNotification(QString message, IdType moleQueueId) * - Logger::logWarning(QString message, IdType moleQueueId) * - Logger::logError(QString message, IdType moleQueueId) * * Each LogEntry contains a user-friendly message, an LogEntryType to identify * the type of log entry, an optional MoleQueue id for any associate Job, and * a timestamp, which is set by the Logger when the entry is added. * * @see Logger */ class LogEntry { public: /// Enumeration of different types of log entries. enum LogEntryType { /// Verbose debugging information. DebugMessage = 0, /// Routine information that is relevant to the user. Notification, /// Non-routine information that is relevant to the user, but does not /// indicate a serious problem. Warning, /// Serious problem that will affect either the MoleQueue application /// or a Job's ability to perform properly. Error }; /** * @brief LogEntry Construct a new log entry. * @param type Type of log message. * @param message_ Descriptive user-visible message for log. * @param moleQueueId_ MoleQueue id of any associated job. * @see Logger::addDebugMessage * @see Logger::addNotification * @see Logger::addWarning * @see Logger::addError */ LogEntry(LogEntryType type, const QString &message_, const IdType &moleQueueId_ = InvalidId); /// Copy the LogEntry @a other into a new LogEntry LogEntry(const LogEntry &other); /// Destroy the log entry virtual ~LogEntry(); /// @return The type of log message. LogEntryType entryType() const {return m_entryType;} /// @return True if this message has type @a type. bool isEntryType(LogEntryType type) const {return m_entryType == type;} /// A user-friendly log message. void setMessage(const QString &message_) { m_message = message_; } /// A user-friendly log message QString message() const { return m_message; } /// The MoleQueue id of the associated job (if any, InvalidId otherwise). void setMoleQueueId(IdType moleQueueId_) { m_moleQueueId = moleQueueId_; } /// The MoleQueue id of the associated job (if any, InvalidId otherwise). IdType moleQueueId() const { return m_moleQueueId; } // Get the timestamp on the LogEntry. const QDateTime & timeStamp() const; friend class MoleQueue::Logger; protected: /// Initialize from data in the QJsonObject. LogEntry(const QJsonObject &json); /// Write this entry's settings to the QJsonObject. void writeSettings(QJsonObject &root) const; /// Set the timestamp on this LogEntry to the current time. void setTimeStamp(); private: QString m_message; IdType m_moleQueueId; LogEntryType m_entryType; QDateTime m_timeStamp; }; } // namespace MoleQueue #endif // MOLEQUEUE_LOGENTRY_H molequeue-0.9.0/molequeue/app/logger.cpp000066400000000000000000000150021323436134600202700ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "logger.h" #include "logentry.h" #include #include #include #include #include #include #include #include namespace MoleQueue { Logger *Logger::m_instance = NULL; Logger::Logger() : m_printDebugMessages(false), m_printNotifications(false), m_printWarnings(false), m_printErrors(false), m_maxEntries(1000), m_newErrorCount(0), m_silenceNewErrors(false), m_logFile(NULL) { // Call destructor when program exits connect(QApplication::instance(), SIGNAL(aboutToQuit()), SLOT(cleanUp())); QFile *lfile = logFile(); if (lfile) { if (!lfile->open(QFile::ReadOnly | QFile::Text)) { QSettings settings; if (settings.value("logWritten", false).toBool()) { qWarning() << "MoleQueue::Logger::~Logger() -- Cannot open log " "file " + lfile->fileName() + "; cannot read log."; } return; } QByteArray logData = lfile->readAll(); lfile->close(); QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(logData, &error); if (error.error != QJsonParseError::NoError) { qWarning() << "MoleQueue::Logger::~Logger() -- Error parsing log file" << lfile->fileName() << ":" << error.errorString() << "(at offset" << error.offset << ")."; return; } if (!doc.isObject()) { qWarning() << "MoleQueue::Logger::~Logger() -- Error parsing log file" << lfile->fileName() << ": Invalid format, expected JSON " "object at top level."; return; } QJsonObject logObject = doc.object(); if (logObject.value("maxEntries").isDouble()) { m_maxEntries = static_cast(logObject.value("maxEntries").toDouble() + 0.5); } if (logObject.value("entries").isArray()) { const QJsonArray &entries = logObject.value("entries").toArray(); foreach (QJsonValue val, entries) { if (val.isObject()) m_log.push_back(LogEntry(val.toObject())); } } } // end if (lfile) } Logger::~Logger() { QFile *lfile = logFile(); if (lfile) { if (!lfile->open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) { qWarning() << "MoleQueue::Logger::~Logger() -- Cannot create log " "file" + lfile->fileName() + "; cannot save log."; return; } QJsonObject root; root.insert("maxEntries", static_cast(m_maxEntries)); QJsonArray entriesArray; foreach(const LogEntry &entry, m_log) { QJsonObject entryObject; entry.writeSettings(entryObject); entriesArray.append(entryObject); } root.insert("entries", entriesArray); lfile->write(QJsonDocument(root).toJson()); lfile->close(); QSettings settings; settings.setValue("logWritten", true); } // end if (lfile) } void Logger::resetNewErrorCount() { Logger *instance = Logger::getInstance(); if (instance->m_newErrorCount == 0) return; emit instance->newErrorCountReset(); instance->m_newErrorCount = 0; } void Logger::cleanUp() { delete m_instance; m_instance = NULL; } QFile *Logger::logFile() { if (!m_logFile) { QSettings settings; QString workDir = settings.value("workingDirectoryBase").toString(); if (workDir.isEmpty()) { qWarning() << "MoleQueue::Logger::~Logger() -- Cannot determine working " "directory."; return NULL; } QDir logDir(workDir + "/log"); if (!logDir.exists()) { if (!logDir.mkpath(logDir.absolutePath())) { qWarning() << "MoleQueue::Logger::~Logger() -- Cannot create log " "directory" + logDir.absolutePath(); return NULL; } } m_logFile = new QFile(logDir.absoluteFilePath("log.json")); } if (m_logFile) { if (m_logFile->isOpen()) m_logFile->close(); } return m_logFile; } Logger *Logger::getInstance() { if (!m_instance) m_instance = new Logger (); return m_instance; } void Logger::handleNewLogEntry(LogEntry &entry) { entry.setTimeStamp(); m_log.push_back(entry); trimLog(); switch (entry.entryType()) { case LogEntry::DebugMessage: handleNewDebugMessage(entry); break; case LogEntry::Notification: handleNewNotification(entry); break; case LogEntry::Warning: handleNewWarning(entry); break; case LogEntry::Error: handleNewError(entry); break; } emit newLogEntry(entry); } inline void Logger::handleNewDebugMessage(const MoleQueue::LogEntry &debug) { if (m_printDebugMessages) { qDebug() << "Debugging message:" << "Message: " << debug.message() << "MoleQueueId: (" << debug.moleQueueId() << ")"; } emit newDebugMessage(debug); } inline void Logger::handleNewNotification(const MoleQueue::LogEntry ¬if) { if (m_printNotifications) { qDebug() << "Notification:" << "Message: " << notif.message() << "MoleQueueId: (" << notif.moleQueueId() << ")"; } emit newNotification(notif); } inline void Logger::handleNewWarning(const MoleQueue::LogEntry &warning) { if (m_printWarnings) { qDebug() << "Warning:" << "Message: " << warning.message() << "MoleQueueId: (" << warning.moleQueueId() << ")"; } emit newWarning(warning); } inline void Logger::handleNewError(const MoleQueue::LogEntry &error) { if (m_printErrors) { qDebug() << "Error occurred:" << "Message: " << error.message() << "MoleQueueId: (" << error.moleQueueId() << ")"; } ++m_newErrorCount; emit newError(error); if (!m_silenceNewErrors && m_newErrorCount == 1) emit firstNewErrorOccurred(); } void Logger::trimLog() { if (m_log.size() > m_maxEntries) { m_log.erase(m_log.begin(), m_log.begin() + (m_log.size() - m_maxEntries)); } } } // namespace MoleQueue molequeue-0.9.0/molequeue/app/logger.h000066400000000000000000000174421323436134600177470ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_LOGGER_H #define MOLEQUEUE_LOGGER_H #include #include "logentry.h" #include class QFile; namespace MoleQueue { /** * @class Logger logger.h * @brief Manage log messages. * @author David C. Lonie * * The singleton Logger class is used to handle log messages in MoleQueue. Log * messages are represented as objects consisting of a user-friendly string, * an enum value representing a subtype, and an optional MoleQueue id for any * associated job. * * There are four levels of log messages: * - DebugMessage: Verbose debugging information. * - Notification: Routine information that is relevant to the user. * - Warning: Non-routine information that is relevant to the user, but does not * indicate a serious problem. * - Error: Serious problem that will affect either the MoleQueue application * or a Job's ability to perform properly. * * New log entries can be submitted using the static Logger::logEntry method or * the convenient logDebugMessage, logNotification, logWarning, or logError. * Each new log entry causes the newLogEntry signal to be emitted, as well as * one of newDebugMessage, newNotification, newWarning, or newError, depending * on the LogEntry type. Details of new log entries will be automatically * sent to qDebug() if the print* methods are set to true (false by default). */ class Logger : public QObject { Q_OBJECT public: /// @return The singleton Logger instance static Logger *getInstance(); /// @return Whether or not to print debugging messages to qDebug. /// Default: false static bool printDebugMessages() { return Logger::getInstance()->m_printDebugMessages; } /// @return Whether or not to print notifications to qDebug. Default: false static bool printNotifications() { return Logger::getInstance()->m_printNotifications; } /// @return Whether or not to print warnings to qDebug. Default: false static bool printWarnings() { return Logger::getInstance()->m_printWarnings; } /// @return Whether or not to print errors to qDebug. Default: false static bool printErrors() { return Logger::getInstance()->m_printErrors; } /// @return The maximum number of entries the Logger will track. /// Default: 1000 static int maxEntries() { return Logger::getInstance()->m_maxEntries; } /// @return The number of new errors that have occurred since the last /// Logger::resetNewErrors call. static int numNewErrors() { return Logger::getInstance()->m_newErrorCount; } signals: /// Emitted when a new debugging message has been added to the log. void newDebugMessage(const MoleQueue::LogEntry &debug); /// Emitted when a new notification has been added to the log. void newNotification(const MoleQueue::LogEntry ¬if); /// Emitted when a new warning has been added to the log. void newWarning(const MoleQueue::LogEntry &warning); /// Emitted when a new error has been added to the log. void newError(const MoleQueue::LogEntry &error); /// Emitted when any new log entry is added to the log. void newLogEntry(const MoleQueue::LogEntry &entry); /// Emitted when the new error count becomes non-zero. Monitor this to check /// for the presence of errors without getting notified about each error. void firstNewErrorOccurred(); /// Emitted when the new error count is reset. void newErrorCountReset(); public slots: /// Add @a entry to the log. static void logEntry(MoleQueue::LogEntry &entry) { Logger::getInstance()->handleNewLogEntry(entry); } /// Add a new log entry to the log. static void logEntry(LogEntry::LogEntryType type, const QString &message, const IdType &moleQueueId = InvalidId) { LogEntry entry(type, message, moleQueueId); Logger::logEntry(entry); } /// Add a new debugging message to the log. static void logDebugMessage(const QString &message, const IdType &moleQueueId = InvalidId) { LogEntry entry(LogEntry::DebugMessage, message, moleQueueId); Logger::logEntry(entry); } /// Add a new notification to the log. static void logNotification(const QString &message, const IdType &moleQueueId = InvalidId) { LogEntry entry(LogEntry::Notification, message, moleQueueId); Logger::logEntry(entry); } /// Add a new warning to the log. static void logWarning(const QString &message, const IdType &moleQueueId = InvalidId) { LogEntry entry(LogEntry::Warning, message, moleQueueId); Logger::logEntry(entry); } /// Add a new error to the log. static void logError(const QString &message, const IdType &moleQueueId = InvalidId) { LogEntry entry(LogEntry::Error, message, moleQueueId); Logger::logEntry(entry); } /// @param print Whether or not to print debugging messages to qDebug. /// Default: false static void setPrintDebugMessages(bool print) { Logger::getInstance()->m_printDebugMessages = print; } /// @param print Whether or not to print notifications to qDebug. Default: /// false static void setPrintNotifications(bool print) { Logger::getInstance()->m_printNotifications = print; } /// @param print Whether or not to print warnings to qDebug. Default: false static void setPrintWarnings(bool print) { Logger::getInstance()->m_printWarnings = print; } /// @param print Whether or not to print errors to qDebug. Default: false static void setPrintErrors(bool print) { Logger::getInstance()->m_printErrors = print; } /// @return A list of all log entries. static QLinkedList log() { return Logger::getInstance()->m_log; } /// @param max The maximum number of entries the Logger will track. /// Default: 1000 static void setMaxEntries(int max) { Logger::getInstance()->m_maxEntries = max; Logger::getInstance()->trimLog(); } /// Remove all entries from the log static void clear() { Logger::getInstance()->m_log.clear(); } /// Reset the number of new errors. static void resetNewErrorCount(); /// @param silence If true, the firstNewErrorOccurred signal will not be /// emitted until this function is called again with false. static void silenceNewErrors(bool silence = true) { Logger::getInstance()->m_silenceNewErrors = silence; } protected slots: void cleanUp(); private: Logger(); ~Logger(); /// Create and return the log file. When finished, close but do not delete the /// object. If an error occurs, a message is printed to qWarning and NULL is /// returned. QFile *logFile(); static Logger *m_instance; void handleNewLogEntry(LogEntry &entry); void handleNewDebugMessage(const MoleQueue::LogEntry &debug); void handleNewNotification(const MoleQueue::LogEntry ¬if); void handleNewWarning(const MoleQueue::LogEntry &warning); void handleNewError(const MoleQueue::LogEntry &error); void trimLog(); bool m_printDebugMessages; bool m_printNotifications; bool m_printWarnings; bool m_printErrors; int m_maxEntries; int m_newErrorCount; bool m_silenceNewErrors; QFile *m_logFile; QLinkedList m_log; }; } // namespace MoleQueue #endif // MOLEQUEUE_LOGGER_H molequeue-0.9.0/molequeue/app/logwindow.cpp000066400000000000000000000170751323436134600210360ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "logwindow.h" #include "ui_logwindow.h" #include "logger.h" #include "idtypeutils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace MoleQueue { LogWindow::LogWindow(QWidget *theParent, IdType moleQueueId) : QDialog(theParent), ui(new Ui::LogWindow), m_log(NULL), m_maxEntries(NULL), m_logEntryBlockFormat(new QTextBlockFormat()), m_timeStampFormat(new QTextCharFormat()), m_debugMessageFormat(new QTextCharFormat()), m_notificationFormat(new QTextCharFormat()), m_warningFormat(new QTextCharFormat()), m_errorFormat(new QTextCharFormat()), m_moleQueueIdFormat(new QTextCharFormat()), m_messageFormat(new QTextCharFormat()), m_moleQueueId(moleQueueId) { createUi(); // Restore geometry QSettings settings; // Store in different location for filtered logs. if (m_moleQueueId == InvalidId) settings.beginGroup("logWindow"); else settings.beginGroup("logWindow/filtered"); restoreGeometry(settings.value("geometry").toByteArray()); settings.endGroup(); setupFormats(); connect(Logger::getInstance(), SIGNAL(newLogEntry(MoleQueue::LogEntry)), this, SLOT(addLogEntry(MoleQueue::LogEntry))); initializeLogText(); } LogWindow::~LogWindow() { // Store geometry QSettings settings; // Store in different location for filtered logs. if (m_moleQueueId == InvalidId) settings.beginGroup("logWindow"); else settings.beginGroup("logWindow/filtered"); settings.setValue("geometry", saveGeometry()); settings.endGroup(); delete ui; delete m_logEntryBlockFormat; delete m_timeStampFormat; delete m_debugMessageFormat; delete m_notificationFormat; delete m_warningFormat; delete m_errorFormat; delete m_moleQueueIdFormat; delete m_messageFormat; } void LogWindow::changeEvent(QEvent *e) { if (e->type() == QEvent::ActivationChange && isActiveWindow()) Logger::resetNewErrorCount(); QDialog::changeEvent(e); } void LogWindow::closeEvent(QCloseEvent *e) { Logger::silenceNewErrors(false); Logger::resetNewErrorCount(); emit aboutToClose(); QDialog::closeEvent(e); } void LogWindow::hideEvent(QHideEvent *e) { Logger::silenceNewErrors(false); Logger::resetNewErrorCount(); QDialog::hideEvent(e); } void LogWindow::showEvent(QShowEvent *e) { Logger::silenceNewErrors(true); Logger::resetNewErrorCount(); QDialog::showEvent(e); } void LogWindow::addLogEntry(const LogEntry &entry) { if (m_moleQueueId != InvalidId && m_moleQueueId != entry.moleQueueId()) return; QString entryType; QTextCharFormat *entryFormat; switch (entry.entryType()) { case LogEntry::DebugMessage: entryType = tr("Debug"); entryFormat = m_debugMessageFormat; break; case LogEntry::Notification: entryType = tr("Notification"); entryFormat = m_notificationFormat; break; case LogEntry::Warning: entryType = tr("Warning"); entryFormat = m_warningFormat; break; case LogEntry::Error: entryType = tr("Error"); entryFormat = m_errorFormat; break; default: entryType = tr("LogEntry"); entryFormat = m_debugMessageFormat; } QTextDocument *doc = m_log->document(); QTextCursor cur(doc); cur.beginEditBlock(); cur.movePosition(QTextCursor::Start); cur.insertBlock(*m_logEntryBlockFormat); cur.insertText(entry.timeStamp().toString("[yyyy-MM-dd hh:mm:ss]"), *m_timeStampFormat); cur.insertText(" "); cur.insertText(QString("%1").arg(entryType, -12), *entryFormat); cur.insertText(" "); if (entry.moleQueueId() == InvalidId) { cur.insertText(tr("Job %1").arg("N/A", -6), *m_moleQueueIdFormat); } else { cur.insertText(tr("Job %1").arg(idTypeToString(entry.moleQueueId()), -6), *m_moleQueueIdFormat); } cur.insertText(" "); // Modify newlines to align with the hanging indent. cur.insertText(entry.message().replace(QRegExp("\\n+"), "\n "), *m_messageFormat); cur.endEditBlock(); } void LogWindow::clearLog() { Logger::clear(); initializeLogText(); } void LogWindow::changeMaxEntries() { Logger::getInstance()->setMaxEntries(m_maxEntries->value()); } void LogWindow::createUi() { ui->setupUi(this); QVBoxLayout *mainLayout = new QVBoxLayout (this); setLayout(mainLayout); m_log = new QTextEdit(this); m_log->setReadOnly(true); mainLayout->addWidget(m_log); // Skip the settings widgets if the molequeueid is set. Update window title if (m_moleQueueId != InvalidId) { setWindowTitle(tr("History for Job %1").arg(idTypeToString(m_moleQueueId))); return; } QHBoxLayout *logSettingsLayout = new QHBoxLayout (); QPushButton *clearLogButton = new QPushButton(tr("&Clear log"), this); connect(clearLogButton, SIGNAL(clicked()), this, SLOT(clearLog())); logSettingsLayout->addWidget(clearLogButton); logSettingsLayout->addStretch(); QLabel *maxEntriesLabel = new QLabel (tr("&Maximum log size:"), this); m_maxEntries = new QSpinBox (this); m_maxEntries->setRange(0, 10000); m_maxEntries->setValue(Logger::maxEntries()); m_maxEntries->setSuffix(QString(" ") + tr("entries")); connect(m_maxEntries, SIGNAL(editingFinished()), this, SLOT(changeMaxEntries())); maxEntriesLabel->setBuddy(m_maxEntries); logSettingsLayout->addWidget(maxEntriesLabel); logSettingsLayout->addWidget(m_maxEntries); mainLayout->addLayout(logSettingsLayout); } void LogWindow::setupFormats() { // Use a hanging indent, aligned with the start of the log message: m_logEntryBlockFormat->setTextIndent(-40); m_logEntryBlockFormat->setIndent(1); m_logEntryBlockFormat->setBottomMargin(5); m_timeStampFormat->setForeground(QBrush(Qt::blue)); m_timeStampFormat->setFontFamily("monospace"); m_debugMessageFormat->setForeground(QBrush(Qt::darkGray)); m_debugMessageFormat->setFontFamily("monospace"); m_notificationFormat->setForeground(QBrush(Qt::darkYellow)); m_notificationFormat->setFontWeight(QFont::Bold); m_notificationFormat->setFontFamily("monospace"); m_warningFormat->setForeground(QBrush(Qt::darkRed)); m_warningFormat->setFontWeight(QFont::Bold); m_warningFormat->setFontFamily("monospace"); m_errorFormat->setForeground(QBrush(Qt::red)); m_errorFormat->setFontWeight(QFont::Bold); m_errorFormat->setFontFamily("monospace"); m_moleQueueIdFormat->setForeground(QBrush(Qt::darkCyan)); m_moleQueueIdFormat->setFontFamily("monospace"); m_messageFormat->setForeground(QBrush(Qt::black)); m_messageFormat->setFontFamily("monospace"); } void LogWindow::initializeLogText() { m_log->clear(); foreach (const LogEntry &entry, Logger::log()) addLogEntry(entry); m_log->moveCursor(QTextCursor::Start); m_log->ensureCursorVisible(); } } // namespace MoleQueue molequeue-0.9.0/molequeue/app/logwindow.h000066400000000000000000000040431323436134600204720ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_LOGWINDOW_H #define MOLEQUEUE_LOGWINDOW_H #include #include "molequeueglobal.h" class QSpinBox; class QTextBlockFormat; class QTextCharFormat; class QTextEdit; namespace Ui { class LogWindow; } namespace MoleQueue { class LogEntry; /// Window that displays log contents class LogWindow : public QDialog { Q_OBJECT public: /// If moleQueueId is set to something other than InvalidId, this window will /// filter its contents to only the entries related to the specified job. LogWindow(QWidget *theParent = 0, IdType moleQueueId = InvalidId); ~LogWindow(); signals: void aboutToClose(); protected: void changeEvent(QEvent *e); void closeEvent(QCloseEvent *); void hideEvent(QHideEvent *); void showEvent(QShowEvent *); private slots: void addLogEntry(const MoleQueue::LogEntry &); void clearLog(); void changeMaxEntries(); private: void createUi(); void setupFormats(); void initializeLogText(); Ui::LogWindow *ui; QTextEdit *m_log; QSpinBox *m_maxEntries; QTextBlockFormat *m_logEntryBlockFormat; QTextCharFormat *m_timeStampFormat; QTextCharFormat *m_debugMessageFormat; QTextCharFormat *m_notificationFormat; QTextCharFormat *m_warningFormat; QTextCharFormat *m_errorFormat; QTextCharFormat *m_moleQueueIdFormat; QTextCharFormat *m_messageFormat; IdType m_moleQueueId; }; } // namespace MoleQueue #endif // MOLEQUEUE_LOGWINDOW_H molequeue-0.9.0/molequeue/app/main.cpp000066400000000000000000000156531323436134600177510ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include #include #include #include #include #include "mainwindow.h" #include void printVersion(); void printUsage(); bool setWorkDir(const QString &workDir); int main(int argc, char *argv[]) { QCoreApplication::setOrganizationName("OpenChemistry"); QCoreApplication::setOrganizationDomain("openchemistry.org"); QCoreApplication::setApplicationName("MoleQueue"); QCoreApplication::setApplicationVersion("0.2.0"); QApplication app(argc, argv); bool customWorkDirSet = false; bool enableRpcKill = false; QString socketName("MoleQueue"); QStringList args = QCoreApplication::arguments(); for (QStringList::const_iterator it = args.constBegin() + 1, itEnd = args.constEnd(); it != itEnd; ++it) { if (*it == "-w" || *it == "--workdir") { if (customWorkDirSet) { qWarning("%s", qPrintable(QObject::tr("More than one -w or -W option set. " "Cannot run in multiple working " "directories!"))); return EXIT_FAILURE; } if (!setWorkDir(it + 1 == itEnd ? QString("") : *(++it))) { return EXIT_FAILURE; } customWorkDirSet = true; continue; } else if (*it == "-s" || *it == "--socketname") { // Wait until all options are parsed in case we end up changing the // settings location. if (it + 1 == itEnd || (it+1)->isEmpty()) { qWarning("%s", qPrintable(QObject::tr("Missing socket name!"))); return EXIT_FAILURE; } socketName = (*++it); continue; } else if (*it == "-v" || *it == "--version") { printVersion(); return EXIT_SUCCESS; } else if (*it == "--rpc-kill") { enableRpcKill = true; continue; } else if (*it == "-h" || *it == "-H" || *it == "--help" || *it == "-help") { printUsage(); return EXIT_SUCCESS; } else { qWarning(qPrintable(QObject::tr("Unrecognized command line option: %s")), qPrintable(*it)); printUsage(); return EXIT_FAILURE; } } // The QSettings object here will point to either the standard config file // or the one set by --workdir. Update any configuration info here: QSettings settings; settings.setValue("socketName", socketName); settings.setValue("enableRpcKill", enableRpcKill); if (!QSystemTrayIcon::isSystemTrayAvailable()) { QMessageBox::critical(0, QObject::tr("MoleQueue"), QObject::tr("System tray not available on this " "system.")); return EXIT_FAILURE; } QApplication::setQuitOnLastWindowClosed(false); // window will show() when the event loop starts. MoleQueue::MainWindow window; return app.exec(); } void printVersion() { qWarning("%s %s", qPrintable(qApp->applicationName()), qPrintable(qApp->applicationVersion())); } void printUsage() { printVersion(); qWarning("%s\n\n%s", qPrintable(QObject::tr("Usage: molequeue [options]")), qPrintable(QObject::tr("Options:"))); const char *format = " %3s %-20s %s"; qWarning(format, "-h,", "--help", qPrintable(QObject::tr("Print version and usage information and " "exit."))); qWarning(format, "", "--rpc-kill", qPrintable(QObject::tr("Allow the app to be killed by a special " "RPC call (testing only)."))); qWarning(format, "-s,", "--socketname [name]", qPrintable(QObject::tr("Name of the socket on which to listen."))); qWarning(format, "-v,", "--version", qPrintable(QObject::tr("Print version information and exit."))); qWarning(format, "-w,", "--workdir [path]", qPrintable(QObject::tr("Run MoleQueue in a custom working " "directory."))); } bool setWorkDir(const QString &workDir) { if (workDir.isEmpty()) { qWarning("%s", qPrintable(QObject::tr("No specified working directory!"))); return false; } QFileInfo dirInfo(QDir::cleanPath(workDir)); if (!dirInfo.exists()) { qWarning(qPrintable(QObject::tr("Specified working directory does not " "exist: '%s'")), qPrintable(workDir)); return false; } if (!dirInfo.isReadable()) { qWarning(qPrintable(QObject::tr("Specified working directory is not " "readable: '%s'")), qPrintable(workDir)); return false; } if (!dirInfo.isWritable()) { qWarning(qPrintable(QObject::tr("Specified working directory is not " "writable: '%s'")), qPrintable(workDir)); return false; } QDir dir(workDir); if (dir.exists("config")) { QFileInfo configInfo(dir, "config"); if (!configInfo.isDir()) { qWarning(qPrintable(QObject::tr("Invalid working directory '%s': " "'%s/config' exists and is not a " "directory!")), qPrintable(workDir), qPrintable(workDir)); return false; } if (!configInfo.isReadable()) { qWarning(qPrintable(QObject::tr("Invalid working directory '%s': " "'%s/config' is not readable!")), qPrintable(workDir), qPrintable(workDir)); return false; } if (!configInfo.isWritable()) { qWarning(qPrintable(QObject::tr("Invalid working directory '%s': " "'%s/config' is not writable!")), qPrintable(workDir), qPrintable(workDir)); return false; } } else { if (!dir.mkdir("config")) { qWarning(qPrintable(QObject::tr("Cannot create directory '%s'. " "Aborting.")), qPrintable(workDir)); return false; } } qDebug(qPrintable(QObject::tr("Running in working directory '%s'...")), qPrintable(dirInfo.absolutePath())); QSettings::setPath(QSettings::NativeFormat, QSettings::UserScope, workDir + "/config/"); QSettings settings; settings.setValue("workingDirectoryBase", workDir); return true; } molequeue-0.9.0/molequeue/app/mainwindow.cpp000066400000000000000000000330111323436134600211650ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "mainwindow.h" #include "ui_mainwindow.h" #include "aboutdialog.h" #include "actionfactorymanager.h" #include "job.h" #include "jobactionfactories/killjobactionfactory.h" #include "jobactionfactories/opendirectoryactionfactory.h" #include "jobactionfactories/removejobactionfactory.h" #include "jobactionfactories/viewjoblogactionfactory.h" #include "jobmanager.h" #include "logentry.h" #include "logger.h" #include "logwindow.h" #include "openwithmanagerdialog.h" #include "queuemanager.h" #include "queuemanagerdialog.h" #include "server.h" #include #include #include #include #include #include #include #include #include namespace MoleQueue { MainWindow::MainWindow() : m_ui(new Ui::MainWindow), m_logWindow(NULL), m_openWithManagerDialog(NULL), m_queueManagerDialog(NULL), m_minimizeAction(NULL), m_maximizeAction(NULL), m_restoreAction(NULL), m_trayIcon(NULL), m_trayIconMenu(NULL), m_statusTotalJobs(new QLabel(this)), m_statusHiddenJobs(new QLabel(this)), m_server(NULL) { QSettings settings; m_server = new Server(this, settings.value("socketName", "MoleQueue").toString()); m_ui->setupUi(this); QIcon icon(":/icons/molequeue.png"); setWindowIcon(icon); createActions(); createActionFactories(); createShortcuts(); createMainMenu(); createTrayIcon(); createJobTable(); createStatusBar(); readSettings(); connect(m_server, SIGNAL(connectionError(MoleQueue::ConnectionListener::Error,QString)), this, SLOT(handleServerConnectionError(MoleQueue::ConnectionListener::Error, QString))); connect(Logger::getInstance(), SIGNAL(firstNewErrorOccurred()), this, SLOT(errorOccurred())); connect(Logger::getInstance(), SIGNAL(newErrorCountReset()), this, SLOT(errorCleared())); connect(m_ui->errorNotificationLabel, SIGNAL(linkActivated(QString)), this, SLOT(handleErrorNotificationLabelAction(QString))); connect(m_server->jobManager(), SIGNAL(jobStateChanged(MoleQueue::Job, MoleQueue::JobState, MoleQueue::JobState)), this, SLOT(notifyJobStateChange(MoleQueue::Job, MoleQueue::JobState, MoleQueue::JobState))); // This will get handled when the event loop // starts and will launch the server. This must be done this way, otherwise // the user may decide to quit the application if the socket is already // in use, and the call to qApp->exit() will be ignored if no event loop is // running (since this class is constructed in main().) QTimer::singleShot(0, this, SLOT(onEventLoopStart())); } MainWindow::~MainWindow() { writeSettings(); delete m_ui; delete m_server; } void MainWindow::setVisible(bool visible) { m_ui->actionMinimize->setEnabled(visible); m_ui->actionMaximize->setEnabled(!isMaximized()); m_ui->actionRestore->setEnabled(isMaximized() || !visible); QMainWindow::setVisible(visible); } void MainWindow::readSettings() { QSettings settings; restoreGeometry(settings.value("geometry").toByteArray()); restoreState(settings.value("windowState").toByteArray()); m_ui->actionViewJobFilter->setChecked( settings.value("viewJobFilter", false).toBool()); m_ui->jobTableWidget->showFilterBar(m_ui->actionViewJobFilter->isChecked()); m_server->readSettings(settings); ActionFactoryManager::instance()->readSettings(settings); } void MainWindow::writeSettings() { QSettings settings; settings.setValue("geometry", saveGeometry()); settings.setValue("windowState", saveState()); settings.setValue("viewJobFilter", m_ui->actionViewJobFilter->isChecked()); m_server->writeSettings(settings); ActionFactoryManager::instance()->writeSettings(settings); } void MainWindow::trayIconActivated(QSystemTrayIcon::ActivationReason reason) { if (reason != QSystemTrayIcon::Context) show(); } void MainWindow::errorOccurred() { /// @todo Change systray icon m_ui->errorNotificationLabel->show(); if (!m_trayIcon->supportsMessages()) return; m_trayIcon->showMessage(tr("An error has occurred in MoleQueue!"), tr("Check the error log for details."), QSystemTrayIcon::Critical); } void MainWindow::errorCleared() { /// @todo Reset system tray icon m_ui->errorNotificationLabel->hide(); } void MainWindow::notifyJobStateChange(const Job &job, JobState oldState, JobState newState) { if (job.popupOnStateChange()) { m_trayIcon->showMessage(tr("Job '%1' is %2") .arg(job.description()) .arg(jobStateToGuiString(job.jobState())), tr("MoleQueue Job #%1 has changed from %2 to %3.") .arg(idTypeToString(job.moleQueueId())) .arg(jobStateToGuiString(oldState)) .arg(jobStateToGuiString(newState)), QSystemTrayIcon::Information, 5000); } } void MainWindow::onEventLoopStart() { // Start the server first -- this may call qApp->exit() if the socket name // is in use and the user opts to quit. m_server->start(); m_trayIcon->show(); m_ui->errorNotificationLabel->hide(); show(); } void MainWindow::showQueueManagerDialog() { if (!m_queueManagerDialog) { m_queueManagerDialog = new QueueManagerDialog(m_server->queueManager(), this); } m_queueManagerDialog->show(); m_queueManagerDialog->raise(); } void MainWindow::showOpenWithManagerDialog() { if (!m_openWithManagerDialog) m_openWithManagerDialog = new OpenWithManagerDialog(this); m_openWithManagerDialog->loadFactories(); m_openWithManagerDialog->show(); m_openWithManagerDialog->raise(); } void MainWindow::showLogWindow() { if (m_logWindow == NULL) m_logWindow = new LogWindow(this); m_logWindow->show(); m_logWindow->raise(); } void MainWindow::handleServerConnectionError(ConnectionListener::Error err, const QString &str) { // handle AddressInUseError by giving user option to replace current socket if (err == ConnectionListener::AddressInUseError) { QStringList choices; choices << tr("There is no other server running. Continue running.") << tr("Oops -- there is an existing server. Terminate the new server."); bool ok; QString choice = QInputDialog::getItem(this, tr("Replace existing MoleQueue server?"), tr("A MoleQueue server appears to already be " "running. How would you like to handle this?"), choices, 0, false, &ok); int index = choices.indexOf(choice); // Terminate if (!ok || index == 1) { hide(); qApp->exit(1); return; } // Take over connection else { m_server->forceStart(); } } // Any other error -- just notify user: else { QMessageBox::warning(this, tr("Server error"), tr("A server error has occurred: '%1'").arg(str)); } } void MainWindow::handleErrorNotificationLabelAction(const QString &action) { if (action == "viewLog") showLogWindow(); else if (action == "clearError") Logger::resetNewErrorCount(); } void MainWindow::jumpToFilterBar() { if (!m_ui->actionViewJobFilter->isChecked()) m_ui->actionViewJobFilter->activate(QAction::Trigger); m_ui->jobTableWidget->focusInFilter(); } void MainWindow::showAdvancedJobFilters() { // Show the job filter if it is hidden if (!m_ui->actionViewJobFilter->isChecked()) m_ui->actionViewJobFilter->activate(QAction::Trigger); m_ui->jobTableWidget->showAdvancedFilterDialog(); } void MainWindow::keyPressEvent(QKeyEvent *e) { // Handle escape key: if (e->key() == Qt::Key_Escape) { if (m_ui->actionViewJobFilter->isChecked()) { m_ui->actionViewJobFilter->activate(QAction::Trigger); e->accept(); return; } } QMainWindow::keyPressEvent(e); } void MainWindow::updateJobCounts(int totalJobs, int shownJobs) { if (totalJobs == 0) { m_statusTotalJobs->hide(); } else { m_statusTotalJobs->setText(tr("%n job(s)", "", totalJobs)); m_statusTotalJobs->show(); } int hiddenJobs = totalJobs - shownJobs; if (hiddenJobs > 0) { m_statusHiddenJobs->setText(tr("%n job(s) are hidden by filters", "", hiddenJobs)); QPalette pal; pal.setColor(QPalette::Foreground, Qt::darkRed); m_statusHiddenJobs->setPalette(pal); m_statusHiddenJobs->show(); } else { m_statusHiddenJobs->hide(); } } void MainWindow::closeEvent(QCloseEvent *theEvent) { QSettings settings; settings.setValue("geometry", saveGeometry()); settings.setValue("windowState", saveState()); if (m_trayIcon->isVisible()) { QMessageBox::information(this, tr("Systray"), tr("The program will keep running in the " "system tray. To terminate the program, " "choose Quit in the context menu " "of the system tray entry.")); hide(); theEvent->ignore(); } } void MainWindow::createActions() { connect(m_ui->actionMinimize, SIGNAL(triggered()), this, SLOT(hide())); connect(m_ui->actionMaximize, SIGNAL(triggered()), this, SLOT(showMaximized())); connect(m_ui->actionRestore, SIGNAL(triggered()), this, SLOT(showNormal())); connect(m_ui->actionUpdateRemoteQueues, SIGNAL(triggered()), m_server->queueManager(), SLOT(updateRemoteQueues())); connect(m_ui->actionViewJobFilter, SIGNAL(toggled(bool)), m_ui->jobTableWidget, SLOT(showFilterBar(bool))); connect(m_ui->actionAdvancedJobFilters, SIGNAL(triggered()), this, SLOT(showAdvancedJobFilters())); connect(m_ui->actionClearFinishedJobs, SIGNAL(triggered()), m_ui->jobTableWidget, SLOT(clearFinishedJobs())); connect(m_ui->actionAbout, SIGNAL(triggered()), SLOT(showAboutDialog())); } void MainWindow::createShortcuts() { new QShortcut(tr("Ctrl+K", "Jump to filter bar"), this, SLOT(jumpToFilterBar()), SLOT(jumpToFilterBar())); } void MainWindow::createMainMenu() { connect(m_ui->actionQueueManager, SIGNAL(triggered()), this, SLOT(showQueueManagerDialog())); connect(m_ui->actionOpenWithManager, SIGNAL(triggered()), this, SLOT(showOpenWithManagerDialog())); connect(m_ui->actionShowLog, SIGNAL(triggered()), this, SLOT(showLogWindow())); connect(m_ui->actionQuit, SIGNAL(triggered()), qApp, SLOT(quit())); } void MainWindow::createTrayIcon() { m_trayIconMenu = new QMenu(this); m_trayIconMenu->addAction(m_ui->actionMinimize); m_trayIconMenu->addAction(m_ui->actionMaximize); m_trayIconMenu->addAction(m_ui->actionRestore); m_trayIconMenu->addSeparator(); m_trayIconMenu->addAction(m_ui->actionQuit); m_trayIcon = new QSystemTrayIcon(this); m_trayIcon->setContextMenu(m_trayIconMenu); QIcon icon(":/icons/molequeue.png"); m_trayIcon->setIcon(icon); connect(m_trayIcon, SIGNAL(messageClicked()), this, SLOT(show())); connect(m_trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, SLOT(trayIconActivated(QSystemTrayIcon::ActivationReason))); } void MainWindow::createJobTable() { connect(m_ui->jobTableWidget, SIGNAL(jobCountsChanged(int, int)), this, SLOT(updateJobCounts(int,int))); m_ui->jobTableWidget->setJobManager(m_server->jobManager()); } void MainWindow::createActionFactories() { ActionFactoryManager *manager = ActionFactoryManager::instance(); manager->setServer(m_server); // Create default factories: OpenDirectoryActionFactory *dirActionFactory = new OpenDirectoryActionFactory(); dirActionFactory->setServer(m_server); manager->addFactory(dirActionFactory); RemoveJobActionFactory *removeActionFactory = new RemoveJobActionFactory(); removeActionFactory->setServer(m_server); manager->addFactory(removeActionFactory); KillJobActionFactory *killActionFactory = new KillJobActionFactory(); killActionFactory->setServer(m_server); manager->addFactory(killActionFactory); ViewJobLogActionFactory *viewJobLogActionFactory = new ViewJobLogActionFactory(); viewJobLogActionFactory->setServer(m_server); viewJobLogActionFactory->setLogWindowParent(this); manager->addFactory(viewJobLogActionFactory); } void MainWindow::createStatusBar() { statusBar()->addWidget(m_statusTotalJobs); statusBar()->addWidget(m_statusHiddenJobs); m_statusHiddenJobs->hide(); statusBar()->show(); } void MainWindow::showAboutDialog() { AboutDialog about(this); about.exec(); } } // End namespace molequeue-0.9.0/molequeue/app/mainwindow.h000066400000000000000000000055051323436134600206410ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_MAINWINDOW_H #define MOLEQUEUE_MAINWINDOW_H #include #include "molequeueglobal.h" #include #include #include class QAction; class QIcon; class QLabel; namespace Ui { class MainWindow; } namespace MoleQueue { class Job; class JobItemModel; class LogEntry; class LogWindow; class OpenWithManagerDialog; class QueueManagerDialog; class Server; /// The main window for the MoleQueue application. class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(); ~MainWindow(); void setVisible(bool visible); public slots: void readSettings(); void writeSettings(); void trayIconActivated(QSystemTrayIcon::ActivationReason); void errorOccurred(); void errorCleared(); void notifyJobStateChange(const MoleQueue::Job &job, MoleQueue::JobState oldState, MoleQueue::JobState newState); protected slots: void showQueueManagerDialog(); void showOpenWithManagerDialog(); void showLogWindow(); void handleServerConnectionError(MoleQueue::ConnectionListener::Error, const QString &); void handleErrorNotificationLabelAction(const QString &action); void jumpToFilterBar(); void showAdvancedJobFilters(); void updateJobCounts(int totalJobs, int shownJobs); /// Used for initialization after the event loop is available. void onEventLoopStart(); protected: void keyPressEvent(QKeyEvent *); void closeEvent(QCloseEvent *theEvent); void createActions(); void createShortcuts(); void createMainMenu(); void createTrayIcon(); void createJobTable(); void createActionFactories(); void createStatusBar(); Ui::MainWindow *m_ui; LogWindow *m_logWindow; OpenWithManagerDialog *m_openWithManagerDialog; QueueManagerDialog *m_queueManagerDialog; QAction *m_minimizeAction; QAction *m_maximizeAction; QAction *m_restoreAction; QSystemTrayIcon *m_trayIcon; QMenu *m_trayIconMenu; QLabel *m_statusTotalJobs; QLabel *m_statusHiddenJobs; Server *m_server; private slots: void showAboutDialog(); }; } // End namespace #endif molequeue-0.9.0/molequeue/app/molequeueconfig.h.in000066400000000000000000000010501323436134600222500ustar00rootroot00000000000000/* Autogenerated header. Edit molequeueconfig.h.in for permanent changes. */ /* Whether or not the client libraries are built. */ #cmakedefine MoleQueue_BUILD_CLIENT /* Whether or not the server application is built. */ #cmakedefine MoleQueue_BUILD_APPLICATION /* Are with building with UIT support */ #cmakedefine MoleQueue_USE_EZHPC_UIT /* The location of the SSL certificates */ #cmakedefine MoleQueue_SSL_CERT_DIR "@MoleQueue_SSL_CERT_DIR@" #cmakedefine MoleQueue_VERSION "@MoleQueue_VERSION@" #define MoleQueue_LIB_DIR "@MoleQueue_LIB_DIR@" molequeue-0.9.0/molequeue/app/molequeueglobal.h000066400000000000000000000124231323436134600216440ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUEGLOBAL_H #define MOLEQUEUEGLOBAL_H #include #include #include #include #include namespace MoleQueue { /// Type for various ids typedef qint64 IdType; /// Constant value used for invalid ids const IdType InvalidId = (std::numeric_limits::max)(); /// Type for list queue/program names. Key is queue, value is list of supported /// programs typedef QHash QueueListType; /** * Enumeration defining states that jobs are allowed to be in. */ enum JobState { /// Unknown status Unknown = -1, /// Initial state of job, should never be entered. None = 0, /// Job has been accepted by the server and is being prepared (Writing input files, etc). Accepted, /// Job is being queued locally, either waiting for local execution or remote submission. QueuedLocal, /// Job has been submitted to a remote queuing system. Submitted, /// Job is pending execution on a remote queuing system. QueuedRemote, /// Job is running locally. RunningLocal, /// Job is running remotely. RunningRemote, /// Job has completed. Finished, /// Job has been terminated at a user request. Canceled, /// Job has been terminated due to an error. Error }; /** * Convert a JobState value to a string. * * @param state JobState * @return C string */ inline const char * jobStateToString(JobState state) { switch (state) { case None: return "None"; case Accepted: return "Accepted"; case QueuedLocal: return "QueuedLocal"; case Submitted: return "Submitted"; case QueuedRemote: return "QueuedRemote"; case RunningLocal: return "RunningLocal"; case RunningRemote: return "RunningRemote"; case Finished: return "Finished"; case Canceled: return "Canceled"; case Error: return "Error"; default: case Unknown: return "Unknown"; } } /** * Convert a JobState value to a string that may be displayed in a GUI. * * @param state JobState * @return C string */ inline const char * jobStateToGuiString(JobState state) { switch (state) { case None: return "None"; case Accepted: return "Accepted"; case QueuedLocal: return "Queued local"; case Submitted: return "Submitted"; case QueuedRemote: return "Queued remote"; case RunningLocal: return "Running local"; case RunningRemote: return "Running remote"; case Finished: return "Finished"; case Canceled: return "Canceled"; case Error: return "Error"; default: case Unknown: return "Unknown"; } } /** * Convert a string to a JobState value. * * @param state JobState string * @return JobState */ inline JobState stringToJobState(const char *str) { if (qstrcmp(str, "None") == 0) return None; else if (qstrcmp(str, "Accepted") == 0) return Accepted; else if (qstrcmp(str, "QueuedLocal") == 0) return QueuedLocal; else if (qstrcmp(str, "Submitted") == 0) return Submitted; else if (qstrcmp(str, "QueuedRemote") == 0) return QueuedRemote; else if (qstrcmp(str, "RunningLocal") == 0) return RunningLocal; else if (qstrcmp(str, "RunningRemote") == 0) return RunningRemote; else if (qstrcmp(str, "Finished") == 0) return Finished; else if (qstrcmp(str, "Canceled") == 0) return Canceled; else if (qstrcmp(str, "Error") == 0) return Error; else return Unknown; } /// @overload inline JobState stringToJobState(const QByteArray &str) { return stringToJobState(str.constData()); } /// @overload inline JobState stringToJobState(const QString &str) { return stringToJobState(qPrintable(str)); } /** * Enumeration defining possible error codes. */ enum ErrorCode { /// No error occurred. NoError = 0, /// Requested queue does not exist. InvalidQueue, /// Requested program does not exist on queue. InvalidProgram, /// Job with specified MoleQueue id does not exist. InvalidMoleQueueId, /// Job is not in the proper state for the requested operation InvalidJobState }; /// Default time in between remote queue updates in minutes. const int DEFAULT_REMOTE_QUEUE_UPDATE_INTERVAL = 3; /// Default number of processor cores for a job const int DEFAULT_NUM_CORES = 1; /// Default walltime limit for a job const int DEFAULT_MAX_WALLTIME = 1440; // Valid names for queues/programs const char VALID_NAME_REG_EXP[] = "[0-9A-za-z()[\\]{}]" "[0-9A-Za-z()[\\]{}\\-_+=.@ ]*"; } // end namespace MoleQueue Q_DECLARE_METATYPE(MoleQueue::IdType) Q_DECLARE_METATYPE(MoleQueue::QueueListType) Q_DECLARE_METATYPE(MoleQueue::JobState) Q_DECLARE_METATYPE(MoleQueue::ErrorCode) #endif // MOLEQUEUEGLOBAL_H molequeue-0.9.0/molequeue/app/opensshcommand.cpp000066400000000000000000000032541323436134600220350ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "opensshcommand.h" #include "terminalprocess.h" #include #include #include namespace MoleQueue { OpenSshCommand::OpenSshCommand(QObject *parentObject) : SshCommand(parentObject, "ssh", "scp") { } OpenSshCommand::~OpenSshCommand() { } QStringList OpenSshCommand::sshArgs() { QStringList args; // Suppress login banners args << "-q"; if (!m_identityFile.isEmpty()) args << "-i" << m_identityFile; if (m_portNumber >= 0 && m_portNumber != 22) args << "-p" << QString::number(m_portNumber); return args; } QStringList OpenSshCommand::scpArgs() { QStringList args; // Suppress login banners args << "-q"; // Ensure the same SSH used for commands is used by scp. args << "-S" << m_sshCommand; if (!m_identityFile.isEmpty()) args << "-i" << m_identityFile; if (m_portNumber >= 0 && m_portNumber != 22) args << "-P" << QString::number(m_portNumber); return args; } } // End namespace molequeue-0.9.0/molequeue/app/opensshcommand.h000066400000000000000000000031041323436134600214740ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef OPENSSHCOMMAND_H #define OPENSSHCOMMAND_H #include "sshcommand.h" namespace MoleQueue { class TerminalProcess; /** * @class OpenSshCommand opensshcommand.h * @brief Concrete implementation of SshCommand using commandline open ssh/scp. * @author Marcus D. Hanwell, David C. Lonie, Chris Harris * * The OpenSshCommand provides an implementation of the SshCommand interface * that calls the commandline ssh and scp executables in a TerminalProcess. * * When writing code that needs ssh functionality, the code should use the * SshConnection interface instead. */ class OpenSshCommand : public SshCommand { Q_OBJECT public: OpenSshCommand(QObject *parentObject = 0); ~OpenSshCommand(); protected: /// @return the arguments to be passed to the SSH command. QStringList sshArgs(); /// @return the arguments to be passed to the SCP command. QStringList scpArgs(); }; } // End namespace #endif molequeue-0.9.0/molequeue/app/openwithexecutablemodel.cpp000066400000000000000000000135051323436134600237370ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "openwithexecutablemodel.h" #include "jobactionfactories/openwithactionfactory.h" namespace { enum Column { FactoryName = 0, Type, Target, COLUMN_COUNT }; } namespace MoleQueue { OpenWithExecutableModel::OpenWithExecutableModel(QObject *parentObject) : QAbstractItemModel(parentObject), m_factories(NULL) { } int OpenWithExecutableModel::rowCount(const QModelIndex &) const { if (!m_factories) return 0; return m_factories->size(); } int OpenWithExecutableModel::columnCount(const QModelIndex &) const { return COLUMN_COUNT; } QVariant OpenWithExecutableModel::data(const QModelIndex &ind, int role) const { if ((role != Qt::DisplayRole && role != Qt::EditRole) || !m_factories || !ind.isValid() || ind.row() >= m_factories->size() || ind.row() < 0 || ind.column() >= COLUMN_COUNT || ind.column() < 0) { return QVariant(); } switch (static_cast(ind.column())) { case FactoryName: return (*m_factories)[ind.row()].name(); case Type: { if (role == Qt::DisplayRole) { switch ((*m_factories)[ind.row()].handlerType()) { default: case OpenWithActionFactory::NoHandler: return tr("N/A"); case OpenWithActionFactory::ExecutableHandler: return tr("EXE", "executable abbreviation"); case OpenWithActionFactory::RpcHandler: return tr("RPC", "remote procedure call abbreviation"); } } else { return (*m_factories)[ind.row()].handlerType(); } } case Target: { OpenWithActionFactory &f((*m_factories)[ind.row()]); switch (f.handlerType()) { default: case OpenWithActionFactory::NoHandler: return QString(); case OpenWithActionFactory::ExecutableHandler: return f.executable(); case OpenWithActionFactory::RpcHandler: return QString("%1@%2").arg(f.rpcMethod(), f.rpcServer()); } } default: break; } return QVariant(); } QVariant OpenWithExecutableModel::headerData( int section, Qt::Orientation orientation, int role) const { if (m_factories && role == Qt::DisplayRole && orientation == Qt::Horizontal) { switch (static_cast(section)) { case FactoryName: return tr("Name"); case Type: return tr("Type"); case Target: return tr("Target"); default: break; } } return QVariant(); } bool OpenWithExecutableModel::insertRows(int row, int count, const QModelIndex &) { if (!m_factories) return false; beginInsertRows(QModelIndex(), row, row + count - 1); for (int i = 0; i < count; ++i) { OpenWithActionFactory newFactory; newFactory.setName(tr("New%1").arg(count == 1 ? QString("") : QString::number(i+1))); // Add a default regex that matches everything newFactory.filePatternsRef() << QRegExp("*", Qt::CaseInsensitive, QRegExp::Wildcard); m_factories->insert(row, newFactory); } endInsertRows(); return true; } bool OpenWithExecutableModel::removeRows( int row, int count, const QModelIndex &) { if (!m_factories) return false; beginRemoveRows(QModelIndex(), row, row + count - 1); for (int i = 0; i < count; ++i) m_factories->removeAt(row); endRemoveRows(); return true; } bool OpenWithExecutableModel::setData(const QModelIndex &ind, const QVariant &value, int role) { if (!m_factories || !ind.isValid() || ind.row() >= m_factories->size() || ind.row() < 0 || ind.column() >= COLUMN_COUNT || ind.column() < 0 || role != Qt::EditRole) { return false; } switch (static_cast(ind.column())) { case FactoryName: (*m_factories)[ind.row()].setName(value.toString()); break; case Type: (*m_factories)[ind.row()].setHandlerType( static_cast(value.toInt())); break; case Target: { OpenWithActionFactory &f((*m_factories)[ind.row()]); switch (f.handlerType()) { default: case OpenWithActionFactory::NoHandler: break; case OpenWithActionFactory::ExecutableHandler: f.setExecutable(value.toString()); break; case OpenWithActionFactory::RpcHandler: { QString rpcSpec(value.toString()); int split(rpcSpec.indexOf('@')); f.setRpcDetails(rpcSpec.mid(split + 1), rpcSpec.left(split)); break; } } } default: break; } emit dataChanged(ind, ind); return true; } Qt::ItemFlags OpenWithExecutableModel::flags(const QModelIndex &) const { return static_cast (Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled); } QModelIndex OpenWithExecutableModel::index(int row, int column, const QModelIndex &p) const { if (p.isValid()) return QModelIndex(); return createIndex(row, column); } QModelIndex OpenWithExecutableModel::parent(const QModelIndex &) const { return QModelIndex(); } void OpenWithExecutableModel::setFactories(QList *factories) { if (m_factories == factories) return; beginResetModel(); m_factories = factories; endResetModel(); } } // namespace MoleQueue molequeue-0.9.0/molequeue/app/openwithexecutablemodel.h000066400000000000000000000036241323436134600234050ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_OPENWITHEXECUTABLEMODEL_H #define MOLEQUEUE_OPENWITHEXECUTABLEMODEL_H #include namespace MoleQueue { class OpenWithActionFactory; /// MVC item model for OpenWithActionFactory configurations. class OpenWithExecutableModel : public QAbstractItemModel { Q_OBJECT public: explicit OpenWithExecutableModel(QObject *parentObject = 0); int rowCount(const QModelIndex &p = QModelIndex()) const; int columnCount(const QModelIndex &p = QModelIndex()) const; QVariant data(const QModelIndex &ind, int role) const; QVariant headerData(int section, Qt::Orientation orientation, int role) const; virtual bool insertRows(int row, int count, const QModelIndex &parent); virtual bool removeRows(int row, int count, const QModelIndex &parent); bool setData(const QModelIndex &ind, const QVariant &value, int role); Qt::ItemFlags flags(const QModelIndex &index) const; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; QModelIndex parent(const QModelIndex &child) const; public slots: void setFactories(QList *factories); protected: QList *m_factories; }; } // namespace MoleQueue #endif // MOLEQUEUE_OPENWITHEXECUTABLEMODEL_H molequeue-0.9.0/molequeue/app/openwithmanagerdialog.cpp000066400000000000000000000476651323436134600234050ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "openwithmanagerdialog.h" #include "ui_openwithmanagerdialog.h" #include "actionfactorymanager.h" #include "jobactionfactories/openwithactionfactory.h" #include "openwithexecutablemodel.h" #include "openwithpatternmodel.h" #include "patterntypedelegate.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace MoleQueue { OpenWithManagerDialog::OpenWithManagerDialog(QWidget *parentObject) : QDialog(parentObject), ui(new Ui::OpenWithManagerDialog), m_factoryModel(new OpenWithExecutableModel(this)), m_patternModel(new OpenWithPatternModel(this)), m_patternMapper(new QDataWidgetMapper(this)), m_handlerMapper(new QDataWidgetMapper(this)), m_patternTypeDelegate(new PatternTypeDelegate(this)), m_dirty(false) { // Setup ui: ui->setupUi(this); // Setup MVC: ui->tableFactories->setModel(m_factoryModel); ui->tablePattern->setModel(m_patternModel); ui->tablePattern->setItemDelegate(m_patternTypeDelegate); ui->comboMatch->setModel(m_patternTypeDelegate->patternTypeModel()); m_handlerMapper->setModel(m_factoryModel); m_handlerMapper->setSubmitPolicy(QDataWidgetMapper::AutoSubmit); m_handlerMapper->addMapping(ui->editName, 0); m_handlerMapper->addMapping(ui->comboType, 1, "currentIndex"); m_handlerMapper->addMapping(ui->editExec, 2); m_patternMapper->setModel(m_patternModel); m_patternMapper->setSubmitPolicy(QDataWidgetMapper::ManualSubmit); m_patternMapper->setItemDelegate(m_patternTypeDelegate); m_patternMapper->addMapping(ui->editPattern, OpenWithPatternModel::PatternCol); m_patternMapper->addMapping(ui->comboMatch, OpenWithPatternModel::PatternTypeCol); m_patternMapper->addMapping(ui->checkCaseSensitive, OpenWithPatternModel::CaseSensitivityCol); // Setup executable completion QFileSystemModel *fsModel = new QFileSystemModel(this); fsModel->setFilter(QDir::Files | QDir::Dirs | QDir::NoDot); fsModel->setRootPath(QDir::rootPath()); QCompleter *fsCompleter = new QCompleter(fsModel, this); ui->editExec->setCompleter(fsCompleter); // Factory GUI: connect(ui->pushAddFactory, SIGNAL(clicked()), this, SLOT(addFactory())); connect(ui->pushRemoveFactory, SIGNAL(clicked()), this, SLOT(removeFactory())); connect(ui->comboType, SIGNAL(currentIndexChanged(int)), this, SLOT(factoryTypeChanged(int))); connect(ui->tableFactories->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(factorySelectionChanged())); // Executable GUI: connect(ui->pushExec, SIGNAL(clicked()), SLOT(browseExecutable())); connect(ui->editExec, SIGNAL(textChanged(QString)), SLOT(testExecutable())); // Pattern GUI: connect(ui->pushAddPattern, SIGNAL(clicked()), this, SLOT(addPattern())); connect(ui->pushRemovePattern, SIGNAL(clicked()), this, SLOT(removePattern())); connect(ui->tablePattern->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(patternSelectionChanged())); connect(ui->pushApplyPattern, SIGNAL(clicked()), m_patternMapper, SLOT(submit())); connect(ui->pushRevertPattern, SIGNAL(clicked()), m_patternMapper, SLOT(revert())); connect(m_patternModel, SIGNAL(rowsInserted(QModelIndex, int, int)), this, SLOT(patternDimensionsChanged())); connect(m_patternModel, SIGNAL(rowsRemoved(QModelIndex, int, int)), this, SLOT(patternDimensionsChanged())); connect(m_patternModel, SIGNAL(modelReset()), this, SLOT(patternDimensionsChanged())); // Test updates: connect(ui->editTest, SIGNAL(textChanged(QString)), this, SLOT(checkTestText())); connect(m_patternModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(checkTestText())); connect(m_patternModel, SIGNAL(layoutChanged()), this, SLOT(checkTestText())); connect(ui->tableFactories->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(checkTestText())); // handle apply button: connect(ui->buttonBox, SIGNAL(clicked(QAbstractButton*)), SLOT(buttonBoxClicked(QAbstractButton*))); // Mark dirty when the data changes. connect(m_factoryModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), SLOT(markDirty())); connect(m_factoryModel, SIGNAL(rowsInserted(QModelIndex, int, int)), SLOT(markDirty())); connect(m_factoryModel, SIGNAL(rowsRemoved(QModelIndex, int, int)), SLOT(markDirty())); connect(m_patternModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), SLOT(markDirty())); connect(m_patternModel, SIGNAL(rowsInserted(QModelIndex, int, int)), SLOT(markDirty())); connect(m_patternModel, SIGNAL(rowsRemoved(QModelIndex, int, int)), SLOT(markDirty())); } OpenWithManagerDialog::~OpenWithManagerDialog() { delete ui; } void OpenWithManagerDialog::loadFactories() { reset(); ActionFactoryManager *manager = ActionFactoryManager::instance(); m_origFactories = manager->factoriesOfType(); foreach (OpenWithActionFactory *factory, m_origFactories) m_factories << *factory; m_factoryModel->setFactories(&m_factories); } void OpenWithManagerDialog::reset() { m_factories.clear(); m_origFactories.clear(); m_factoryModel->setFactories(NULL); m_patternModel->setRegExps(NULL); setFactoryGuiEnabled(false); setPatternGuiEnabled(false); markClean(); } bool OpenWithManagerDialog::apply() { // Check that all factories are using valid executables: int index = -1; foreach (const OpenWithActionFactory &factory, m_factories) { ++index; // Skip non-executable handlers if (factory.handlerType() != OpenWithActionFactory::ExecutableHandler) continue; QString reason; QString name = factory.name(); QString executable = factory.executable(); QString executableFilePath; switch (validateExecutable(executable, executableFilePath)) { case ExecOk: break; case ExecNotExec: reason = tr("File is not executable: %1").arg(executableFilePath); break; case ExecInvalidPath: reason = tr("File not found in specified path."); break; case ExecNotFound: reason = tr("No file in system path named '%1'.").arg(executable); break; } if (reason.isEmpty()) continue; QMessageBox::StandardButton response = QMessageBox::warning(this, name, tr("An issue was found with the executable for " "'%1':\n\n%2\n\nWould you like to change the " "executable now?").arg(name, reason), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); if (response == QMessageBox::No) continue; ui->tableFactories->selectRow(index); ui->editExec->selectAll(); ui->editExec->setFocus(); return false; } // Delete the original factories from the manager and replace them with our // new ones ActionFactoryManager *manager = ActionFactoryManager::instance(); foreach (OpenWithActionFactory *factory, m_origFactories) manager->removeFactory(factory); foreach (OpenWithActionFactory factory, m_factories) manager->addFactory(new OpenWithActionFactory(factory)); loadFactories(); return true; } void OpenWithManagerDialog::accept() { if (!apply()) return; reset(); QDialog::accept(); } void OpenWithManagerDialog::reject() { reset(); QDialog::reject(); } void OpenWithManagerDialog::closeEvent(QCloseEvent *e) { // Ensure that all forms are submitted if (QWidget *focus = this->focusWidget()) focus->clearFocus(); if (m_dirty) { // apply or discard changes? QMessageBox::StandardButton reply = QMessageBox::warning(this, tr("Unsaved changes"), tr("Your changes have not been saved. Would you " "like to save or discard them?"), QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Save); switch (reply) { case QMessageBox::Cancel: e->ignore(); return; case QMessageBox::Save: if (!apply()) return; case QMessageBox::NoButton: case QMessageBox::Discard: default: break; } } QDialog::closeEvent(e); } void OpenWithManagerDialog::buttonBoxClicked(QAbstractButton *button) { // "Ok" and "Cancel" are directly connected to accept() and reject(), so only // check for "apply" here: if (button == ui->buttonBox->button(QDialogButtonBox::Apply)) apply(); } void OpenWithManagerDialog::markClean() { m_dirty = false; ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); } void OpenWithManagerDialog::markDirty() { m_dirty = true; ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); } void OpenWithManagerDialog::addFactory() { QModelIndexList sel = selectedFactoryIndices(); int index = -1; if (sel.size() == m_factoryModel->columnCount()) index = sel.first().row(); if (index + 1 > m_factoryModel->rowCount(QModelIndex()) || index < 0) index = m_factoryModel->rowCount(QModelIndex()); m_factoryModel->insertRow(index); ui->tableFactories->selectionModel()->select( m_factoryModel->index(index, 0), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); } void OpenWithManagerDialog::removeFactory() { QModelIndexList sel = selectedFactoryIndices(); if (sel.size() != m_factoryModel->columnCount()) return; int index = sel.first().row(); if (index + 1 > m_factoryModel->rowCount(QModelIndex()) || index < 0) return; m_factoryModel->removeRow(index); } void OpenWithManagerDialog::factoryTypeChanged(int type) { ui->stackHandler->setCurrentIndex(type); switch (static_cast(type)) { default: case OpenWithActionFactory::NoHandler: case OpenWithActionFactory::ExecutableHandler: m_handlerMapper->addMapping(ui->editExec, 2); break; case OpenWithActionFactory::RpcHandler: m_handlerMapper->addMapping(ui->editRpc, 2); break; } } void OpenWithManagerDialog::browseExecutable() { QString fileName = ui->editExec->text(); QFileInfo info(fileName); QString initialPath; if (!fileName.isEmpty()) { // If the executable name is not an absolute path, try to look it up in PATH if (info.isAbsolute()) { initialPath = info.absolutePath(); } else { QString absoluteFilePath = searchSystemPathForFile(info.fileName()); // Found the path; initialize the file dialog to it if (!absoluteFilePath.isEmpty()) { ui->editExec->setText(absoluteFilePath); m_handlerMapper->submit(); initialPath = absoluteFilePath; } } } // If we didn't find a path above, just use the user's home directory. if (initialPath.isEmpty()) initialPath = QDir::homePath(); QString newFilePath = QFileDialog::getOpenFileName( this, tr("Select executable"), initialPath); if (!newFilePath.isEmpty()) { ui->editExec->setText(newFilePath); m_handlerMapper->submit(); } testExecutable(); } OpenWithManagerDialog::ExecutableStatus OpenWithManagerDialog::validateExecutable(const QString &executable) { QString tmp; return validateExecutable(executable, tmp); } OpenWithManagerDialog::ExecutableStatus OpenWithManagerDialog::validateExecutable(const QString &executable, QString &executableFilePath) { QFileInfo info(executable); if (info.isAbsolute()) { executableFilePath = info.absoluteFilePath(); if (!info.exists() || !info.isFile()) return ExecInvalidPath; else if (!info.isExecutable()) return ExecNotExec; return ExecOk; } else { executableFilePath = searchSystemPathForFile(executable); info = QFileInfo(executableFilePath); if (executableFilePath.isEmpty() || !info.isFile()) return ExecNotFound; else if (!info.isExecutable()) return ExecNotExec; return ExecOk; } } void OpenWithManagerDialog::testExecutable() { switch (validateExecutable(ui->editExec->text())) { case ExecOk: testExecutableMatch(); break; default: case ExecNotExec: case ExecInvalidPath: case ExecNotFound: testExecutableNoMatch(); break; } } void OpenWithManagerDialog::testExecutableMatch() { QPalette pal; pal.setColor(QPalette::Text, Qt::black); ui->editExec->setPalette(pal); } void OpenWithManagerDialog::testExecutableNoMatch() { QPalette pal; pal.setColor(QPalette::Text, Qt::red); ui->editExec->setPalette(pal); } void OpenWithManagerDialog::factorySelectionChanged() { // Get selected executable QModelIndexList sel = selectedFactoryIndices(); int index = -1; if (sel.size() == m_factoryModel->columnCount()) index = sel.first().row(); // If valid, set the regexp list if (index >= 0 && index < m_factoryModel->rowCount(QModelIndex())) { setFactoryGuiEnabled(true); setPatternGuiEnabled(true); m_patternModel->setRegExps(&m_factories[index].filePatternsRef()); m_patternMapper->toFirst(); } // otherwise, clear the regexp list and disable the pattern GUI else { setFactoryGuiEnabled(false); setPatternGuiEnabled(false); m_patternModel->setRegExps(NULL); } // Update the execMapper if (sel.size()) m_handlerMapper->setCurrentIndex(sel.first().row()); } void OpenWithManagerDialog::setFactoryGuiEnabled(bool enable) { ui->editExec->setEnabled(enable); ui->labelExec->setEnabled(enable); ui->pushExec->setEnabled(enable); ui->editExec->setEnabled(enable); ui->editName->setEnabled(enable); ui->labelName->setEnabled(enable); ui->labelType->setEnabled(enable); ui->comboType->setEnabled(enable); ui->labelRpc->setEnabled(enable); ui->editRpc->setEnabled(enable); if (!enable) { ui->editExec->blockSignals(true); ui->editExec->clear(); ui->editExec->blockSignals(false); ui->editName->blockSignals(true); ui->editName->clear(); ui->editName->blockSignals(false); } } void OpenWithManagerDialog::addPattern() { QModelIndexList sel = selectedPatternIndices(); int index = -1; // Should hold a single row: if (sel.size() == OpenWithPatternModel::COLUMN_COUNT) index = sel.first().row(); if (index+1 > m_patternModel->rowCount(QModelIndex()) || index < 0) index = m_patternModel->rowCount(QModelIndex()); m_patternModel->insertRow(index); ui->tablePattern->selectionModel()->select( m_patternModel->index(index, 0, QModelIndex()), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); } void OpenWithManagerDialog::removePattern() { QModelIndexList sel = selectedPatternIndices(); // Should hold a single row: if (sel.size() != OpenWithPatternModel::COLUMN_COUNT) return; int index = sel.first().row(); if (index < 0 || index >= m_patternModel->rowCount(QModelIndex())) return; m_patternModel->removeRow(index); } void OpenWithManagerDialog::patternSelectionChanged() { QModelIndexList sel = selectedPatternIndices(); if (sel.size()) m_patternMapper->setCurrentIndex(sel.first().row()); } void OpenWithManagerDialog::patternDimensionsChanged() { ui->tablePattern->horizontalHeader()->setSectionResizeMode( OpenWithPatternModel::PatternCol, QHeaderView::Stretch); } void OpenWithManagerDialog::setPatternGuiEnabled(bool enable) { ui->groupPattern->setEnabled(enable); ui->labelPattern->setEnabled(enable); ui->editPattern->setEnabled(enable); ui->comboMatch->setEnabled(enable); ui->checkCaseSensitive->setEnabled(enable); ui->pushApplyPattern->setEnabled(enable); ui->pushRevertPattern->setEnabled(enable); // also clear edits if disabling: if (!enable) { ui->editPattern->blockSignals(true); ui->editPattern->clear(); ui->editPattern->blockSignals(false); ui->comboMatch->blockSignals(true); ui->comboMatch->setCurrentIndex(0); ui->comboMatch->blockSignals(false); ui->checkCaseSensitive->blockSignals(false); ui->checkCaseSensitive->setEnabled(enable); ui->checkCaseSensitive->blockSignals(true); } } void OpenWithManagerDialog::checkTestText() { OpenWithActionFactory *factory = selectedFactory(); if (!factory) { testTextNoMatch(); return; } const QString testText = ui->editTest->text(); foreach (const QRegExp ®exp, factory->filePatterns()) { if (regexp.indexIn(testText) >= 0) { testTextMatch(); return; } } testTextNoMatch(); return; } void OpenWithManagerDialog::testTextMatch() { QPalette pal; pal.setColor(QPalette::Text, Qt::black); ui->editTest->setPalette(pal); } void OpenWithManagerDialog::testTextNoMatch() { QPalette pal; pal.setColor(QPalette::Text, Qt::red); ui->editTest->setPalette(pal); } QString OpenWithManagerDialog::searchSystemPathForFile(const QString &exec) { QString result; QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); if (!env.contains("PATH")) return result; static QRegExp pathSplitter = QRegExp( #ifdef Q_OS_WIN32 ";" #else // WIN32 ":" #endif// WIN32 ); QStringList paths = env.value("PATH").split(pathSplitter, QString::SkipEmptyParts); foreach (const QString &path, paths) { QFileInfo info(QUrl::fromLocalFile(path + "/" + exec).toLocalFile()); if (!info.exists() || !info.isFile()) { continue; } result = info.absoluteFilePath(); break; } return result; } QModelIndexList OpenWithManagerDialog::selectedFactoryIndices() const { return ui->tableFactories->selectionModel()->selectedIndexes(); } QModelIndexList OpenWithManagerDialog::selectedPatternIndices() const { return ui->tablePattern->selectionModel()->selectedIndexes(); } OpenWithActionFactory *OpenWithManagerDialog::selectedFactory() { QModelIndexList sel = selectedFactoryIndices(); if (sel.size() != m_factoryModel->columnCount()) return NULL; int index = sel.first().row(); if (index < 0 || index >= m_factories.size()) return NULL; return &m_factories[index]; } QRegExp *OpenWithManagerDialog::selectedRegExp() { QModelIndexList sel = selectedPatternIndices(); // Should hold a single row: if (sel.size() != OpenWithPatternModel::COLUMN_COUNT) return NULL; int index = sel.first().row(); OpenWithActionFactory *factory = selectedFactory(); if (!factory) return NULL; if (index < 0 || index >= factory->filePatternsRef().size()) return NULL; return &factory->filePatternsRef()[index]; } void OpenWithManagerDialog::keyPressEvent(QKeyEvent *ev) { switch (ev->key()) { // By default, the escape key bypasses the close event, but we still want to // check if the settings widget is dirty. case Qt::Key_Escape: ev->accept(); close(); break; // Disable forwarding of enter and return to Ok/Cancel buttons. Too easy to // accidentally close the dialog while modifying line edits. case Qt::Key_Return: case Qt::Key_Enter: break; default: QDialog::keyPressEvent(ev); } } } // end namespace MoleQueue molequeue-0.9.0/molequeue/app/openwithmanagerdialog.h000066400000000000000000000071021323436134600230300ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef OPENWITHMANAGERDIALOG_H #define OPENWITHMANAGERDIALOG_H #include #include class QAbstractButton; class QDataWidgetMapper; class QItemDelegate; namespace Ui { class OpenWithManagerDialog; } namespace MoleQueue { class JobActionFactory; class OpenWithActionFactory; class OpenWithExecutableModel; class OpenWithPatternModel; class PatternTypeDelegate; /// @brief Dialog window for configuring OpenWithActionFactory objects. class OpenWithManagerDialog : public QDialog { Q_OBJECT public: explicit OpenWithManagerDialog(QWidget *parentObject = 0); ~OpenWithManagerDialog(); void loadFactories(); void reset(); /** Return value is false if user refuses to accept changes. */ bool apply(); void accept(); void reject(); protected: void closeEvent(QCloseEvent *); void keyPressEvent(QKeyEvent *); private: /** Return values for validateExecutable() */ enum ExecutableStatus { /** Executable is found and has correct permissions. */ ExecOk = 0, /** Executable is not marked executable by the filesystem. */ ExecNotExec, /** The executable cannot be found in the specified path. */ ExecInvalidPath, /** The executable cannot be found in the system path. */ ExecNotFound }; private slots: void buttonBoxClicked(QAbstractButton*); void markClean(); void markDirty(); void addFactory(); void removeFactory(); void factoryTypeChanged(int type); void browseExecutable(); ExecutableStatus validateExecutable(const QString &executable); ExecutableStatus validateExecutable(const QString &executable, QString &executableFilePath); void testExecutable(); void testExecutableMatch(); void testExecutableNoMatch(); void factorySelectionChanged(); void setFactoryGuiEnabled(bool enable = true); void addPattern(); void removePattern(); void patternSelectionChanged(); void patternDimensionsChanged(); void setPatternGuiEnabled(bool enable = true); void checkTestText(); void testTextMatch(); void testTextNoMatch(); private: /** * @brief Search the environment variable PATH for a file with the specified * name. * @param exec The name of the file. * @return The absolute path to the file on the system, or a null QString if * not found. */ static QString searchSystemPathForFile(const QString &exec); QModelIndexList selectedFactoryIndices() const; QModelIndexList selectedPatternIndices() const; OpenWithActionFactory *selectedFactory(); QRegExp *selectedRegExp(); Ui::OpenWithManagerDialog *ui; QList m_factories; QList m_origFactories; OpenWithExecutableModel *m_factoryModel; OpenWithPatternModel *m_patternModel; QDataWidgetMapper *m_patternMapper; QDataWidgetMapper *m_handlerMapper; PatternTypeDelegate *m_patternTypeDelegate; bool m_dirty; }; } // end namespace MoleQueue #endif // OPENWITHMANAGERDIALOG_H molequeue-0.9.0/molequeue/app/openwithpatternmodel.cpp000066400000000000000000000166031323436134600232750ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "openwithpatternmodel.h" #include namespace MoleQueue { OpenWithPatternModel::OpenWithPatternModel(QObject *parentObject) : QAbstractItemModel(parentObject), m_regexps(NULL) { } int OpenWithPatternModel::rowCount(const QModelIndex &) const { if (!m_regexps) return 0; return m_regexps->size(); } int OpenWithPatternModel::columnCount(const QModelIndex &) const { if (!m_regexps) return 0; return COLUMN_COUNT; } QVariant OpenWithPatternModel::data(const QModelIndex &ind, int role) const { if ((role != Qt::DisplayRole && role != Qt::EditRole && role != Qt::CheckStateRole && role != ComboIndexRole) || !indexIsValid(ind)) return QVariant(); QRegExp ®exp = (*m_regexps)[ind.row()]; if (role == Qt::CheckStateRole) { if (ind.column() == CaseSensitivityCol) { return regexp.caseSensitivity() == Qt::CaseSensitive ? Qt::Checked : Qt::Unchecked; } else { return QVariant(); } } if (role == ComboIndexRole) { if (ind.column() == PatternTypeCol) { switch (regexp.patternSyntax()) { default: case QRegExp::Wildcard: case QRegExp::WildcardUnix: case QRegExp::W3CXmlSchema11: case QRegExp::FixedString: return static_cast(WildCard); case QRegExp::RegExp: case QRegExp::RegExp2: return static_cast(RegExp); } } } switch (static_cast(ind.column())) { case PatternCol: return regexp.pattern(); case PatternTypeCol: switch (regexp.patternSyntax()) { default: case QRegExp::Wildcard: case QRegExp::WildcardUnix: case QRegExp::W3CXmlSchema11: case QRegExp::FixedString: return tr("WildCard"); case QRegExp::RegExp: case QRegExp::RegExp2: return tr("RegExp"); } case CaseSensitivityCol: if (role == Qt::DisplayRole) { return regexp.caseSensitivity() == Qt::CaseSensitive ? tr("Sensitive", "Case sensitive") : tr("Insensitive", "Case insensitive"); } else { return regexp.caseSensitivity() == Qt::CaseSensitive; } default: // Should not happen... return QVariant(); } } bool OpenWithPatternModel::setData(const QModelIndex &ind, const QVariant &value, int role) { if ((role != Qt::EditRole && role != Qt::CheckStateRole) || !indexIsValid(ind)) { return false; } QRegExp ®exp = (*m_regexps)[ind.row()]; if (role == Qt::CheckStateRole) { if (value.canConvert(QVariant::Int)) { Qt::CheckState state = static_cast(value.toInt()); if (ind.column() == CaseSensitivityCol) { regexp.setCaseSensitivity(state == Qt::Checked ? Qt::CaseSensitive : Qt::CaseInsensitive); emit dataChanged(ind, ind); return true; } } if (value.canConvert(QVariant::Bool)) { if (ind.column() == CaseSensitivityCol) { regexp.setCaseSensitivity(value.toBool() ? Qt::CaseSensitive : Qt::CaseInsensitive); emit dataChanged(ind, ind); return true; } } else { return false; } } switch (static_cast(ind.column())) { case PatternCol: if (value.canConvert(QVariant::String)) { regexp.setPattern(value.toString()); emit dataChanged(ind, ind); return true; } else { return false; } case PatternTypeCol: if (value.type() == QVariant::String) { QString str = value.toString().simplified(); if (!str.isEmpty()) { QChar firstChar = str.at(0).toLower(); if (firstChar == QChar('w')) { regexp.setPatternSyntax(QRegExp::Wildcard); emit dataChanged(ind, ind); return true; } else if (firstChar == QChar('r')) { regexp.setPatternSyntax(QRegExp::RegExp); emit dataChanged(ind, ind); return true; } } } else if (value.canConvert(QVariant::Int)) { switch (static_cast(value.toInt())) { case OpenWithPatternModel::WildCard: regexp.setPatternSyntax(QRegExp::Wildcard); emit dataChanged(ind, ind); return true; case OpenWithPatternModel::RegExp: regexp.setPatternSyntax(QRegExp::RegExp); emit dataChanged(ind, ind); return true; default: return false; } } return false; case CaseSensitivityCol: if (value.canConvert(QVariant::Bool)) { regexp.setCaseSensitivity(value.toBool() ? Qt::CaseSensitive : Qt::CaseInsensitive); emit dataChanged(ind, ind); return true; } return false; default: // Should not happen... return false; } } QVariant OpenWithPatternModel::headerData( int section, Qt::Orientation orientation, int role) const { if (!m_regexps || orientation != Qt::Horizontal || role != Qt::DisplayRole || section < 0 || section >= COLUMN_COUNT) return QVariant(); switch (static_cast(section)) { case PatternCol: return tr("Pattern"); case PatternTypeCol: return tr("Type"); case CaseSensitivityCol: return tr("Case Sensitive"); default: // Should not happen... return QVariant(); } } bool OpenWithPatternModel::insertRows(int row, int count, const QModelIndex &) { if (!m_regexps) return false; beginInsertRows(QModelIndex(), row, row + count - 1); for (int i = 0; i < count; ++i) { QRegExp newRegExp ("*.*", Qt::CaseInsensitive, QRegExp::Wildcard); m_regexps->insert(row, newRegExp); } endInsertRows(); return true; } bool OpenWithPatternModel::removeRows(int row, int count, const QModelIndex &) { if (!m_regexps) return false; beginRemoveRows(QModelIndex(), row, row + count - 1); for (int i = 0; i < count; ++i) m_regexps->removeAt(row); endRemoveRows(); return true; } Qt::ItemFlags OpenWithPatternModel::flags(const QModelIndex &ind) const { Qt::ItemFlags result; if (ind.column() == CaseSensitivityCol) result |= Qt::ItemIsUserCheckable; else result |= Qt::ItemIsEditable; result |= static_cast(Qt::ItemIsSelectable|Qt::ItemIsEnabled); return result; } void OpenWithPatternModel::setRegExps(QList *regexps) { if (regexps == m_regexps) return; beginResetModel(); m_regexps = regexps; endResetModel(); } bool OpenWithPatternModel::indexIsValid(const QModelIndex &ind) const { return (m_regexps && ind.isValid() && ind.row() >= 0 && ind.row() < m_regexps->size() && ind.column() >= 0 && ind.column() < COLUMN_COUNT); } } // end namespace MoleQueue molequeue-0.9.0/molequeue/app/openwithpatternmodel.h000066400000000000000000000043061323436134600227370ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_OPENWITHPATTERNMODEL_H #define MOLEQUEUE_OPENWITHPATTERNMODEL_H #include #include #include namespace MoleQueue { class ProgrammableOpenWithActionFactory; /// @brief MVC item model for interacting with ProgrammableOpenWithActionFactory /// output file filters. class OpenWithPatternModel : public QAbstractItemModel { Q_OBJECT public: explicit OpenWithPatternModel(QObject *parentObject = 0); int rowCount(const QModelIndex &parent) const; int columnCount(const QModelIndex &parent) const; QVariant data(const QModelIndex &ind, int role) const; bool setData(const QModelIndex &index, const QVariant &value, int role); QVariant headerData(int section, Qt::Orientation orientation, int role) const; bool insertRows(int row, int count, const QModelIndex &parent); bool removeRows(int row, int count, const QModelIndex &parent); Qt::ItemFlags flags(const QModelIndex &index) const; QModelIndex index(int row, int column, const QModelIndex &) const { return createIndex(row, column); } QModelIndex parent(const QModelIndex &) const { return QModelIndex(); } enum ColumnType { PatternCol, PatternTypeCol, CaseSensitivityCol, COLUMN_COUNT }; enum PatternType { WildCard = 0, RegExp, PATTERNTYPE_COUNT }; enum CustomRoleType { ComboIndexRole = Qt::UserRole }; void setRegExps(QList *regexps); protected: bool indexIsValid(const QModelIndex &ind) const; QList *m_regexps; }; } // end namespace MoleQueue #endif molequeue-0.9.0/molequeue/app/patterntypedelegate.cpp000066400000000000000000000061751323436134600230760ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "patterntypedelegate.h" #include "openwithpatternmodel.h" // For enums #include #include #include namespace MoleQueue { PatternTypeDelegate::PatternTypeDelegate(QObject *parentObject) : QItemDelegate(parentObject), m_patternTypeModel(new QStringListModel (this)) { QStringList patternTypes; for (int i = 0; i < OpenWithPatternModel::PATTERNTYPE_COUNT; ++i) patternTypes.push_back("--Unknown--"); patternTypes[OpenWithPatternModel::WildCard] = tr("WildCard"); patternTypes[OpenWithPatternModel::RegExp] = tr("RegExp"); m_patternTypeModel->setStringList(patternTypes); } QWidget *PatternTypeDelegate::createEditor(QWidget *parentWidget, const QStyleOptionViewItem &option, const QModelIndex &index) const { if (index.column() == OpenWithPatternModel::PatternTypeCol) { QComboBox *combo = new QComboBox (parentWidget); combo->setModel(patternTypeModel()); return combo; } return QItemDelegate::createEditor(parentWidget, option, index); } void PatternTypeDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const { if (index.column() == OpenWithPatternModel::PatternTypeCol) { editor->setGeometry(option.rect); return; } QItemDelegate::updateEditorGeometry(editor, option, index); } void PatternTypeDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { if (index.column() == OpenWithPatternModel::PatternTypeCol) { if (editor->property("currentIndex").isValid()) { QVariant value = index.data(OpenWithPatternModel::ComboIndexRole); editor->setProperty("currentIndex", value); return; } } QItemDelegate::setEditorData(editor, index); } void PatternTypeDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { if (index.column() == OpenWithPatternModel::PatternTypeCol) { if (editor->property("currentIndex").isValid()) { QVariant value = editor->property("currentIndex"); if (value.isValid()) { model->setData(index, value); return; } } } QItemDelegate::setModelData(editor, model, index); } } // namespace MoleQueue molequeue-0.9.0/molequeue/app/patterntypedelegate.h000066400000000000000000000032201323436134600225270ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_PATTERNTYPEDELEGATE_H #define MOLEQUEUE_PATTERNTYPEDELEGATE_H #include class QStringListModel; namespace MoleQueue { /// MVC delegate to control ProgrammableOpenWithActionFactory patterns. class PatternTypeDelegate : public QItemDelegate { Q_OBJECT public: explicit PatternTypeDelegate(QObject *parentObject = 0); QWidget *createEditor(QWidget *parentWidget, const QStyleOptionViewItem &option, const QModelIndex &index) const; void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const; void setEditorData(QWidget *editor, const QModelIndex &index) const; void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; QStringListModel * patternTypeModel() const { return m_patternTypeModel; } protected: QStringListModel *m_patternTypeModel; }; } // namespace MoleQueue #endif // MOLEQUEUE_PATTERNTYPEDELEGATE_H molequeue-0.9.0/molequeue/app/pluginmanager.cpp000066400000000000000000000115631323436134600216520ustar00rootroot00000000000000/****************************************************************************** This source file is part of the Avogadro project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "pluginmanager.h" #include "molequeueconfig.h" #include #include #include #include #include #include #include namespace MoleQueue { namespace { // Compiler initializes this static pointer to 0. static PluginManager *pluginManagerInstance; } PluginManager::PluginManager(QObject *p) : QObject(p) { QString libDir(MoleQueue_LIB_DIR); // http://doc.qt.digia.com/qt/deployment-plugins.html#debugging-plugins bool debugPlugins = !qgetenv("QT_DEBUG_PLUGINS").isEmpty(); QDir baseDir(QCoreApplication::applicationDirPath() + "/.."); m_relativeToApp = "/../" + libDir + "/molequeue/plugins"; #ifdef __APPLE__ // But if NOT running from the installed bundle on the Mac, the plugins are // relative to the build directory instead: if (!QFileInfo(baseDir.absolutePath() + "/Resources/qt.conf").exists()) { QDir buildDir(QCoreApplication::applicationDirPath() + "/../../../.."); baseDir = buildDir; if (debugPlugins) qDebug() << " using buildDir:" << buildDir.absolutePath(); } #endif // For multi configuration types add correct one to search path. #ifdef MULTI_CONFIG_BUILD // First extract the build type (the name of the application dir). QDir appDir(QCoreApplication::applicationDirPath()); QString buildType = appDir.dirName(); QDir condir(QCoreApplication::applicationDirPath() + "/../../" + libDir + "/molequeue/plugins/" + buildType); m_pluginDirs.append(condir.absolutePath()); #endif // If the environment variable is set, use that as the base directory. QByteArray pluginDir = qgetenv("MOLEQUEUE_PLUGIN_DIR"); if (!pluginDir.isEmpty()) baseDir.setPath(pluginDir); if (debugPlugins) qDebug() << " baseDir:" << baseDir.absolutePath(); QDir pluginsDir(baseDir.absolutePath() + "/" + libDir + "/molequeue/plugins"); m_pluginDirs.append(pluginsDir.absolutePath()); if (debugPlugins) { qDebug() << " pluginsDir:" << pluginsDir.absolutePath(); int count = 0; foreach(const QString &pluginPath, pluginsDir.entryList(QDir::Files)) { ++count; qDebug() << " " << pluginsDir.absolutePath() + "/" + pluginPath; } if (count > 0) qDebug() << " " << count << "files found in" << pluginsDir.absolutePath(); else qDebug() << " no plugin files found in" << pluginsDir.absolutePath(); } } PluginManager::~PluginManager() { } PluginManager * PluginManager::instance() { static QMutex mutex; if (!pluginManagerInstance) { mutex.lock(); if (!pluginManagerInstance) pluginManagerInstance = new PluginManager(QCoreApplication::instance()); mutex.unlock(); } return pluginManagerInstance; } void PluginManager::load() { foreach(const QString &dir, m_pluginDirs) load(dir); } void PluginManager::load(const QString &path) { QDir dir(path); qDebug() << "Checking for plugins in" << path; qDebug() << dir.entryList(QDir::Files); foreach(const QString &pluginPath, dir.entryList(QDir::Files)) { QPluginLoader pluginLoader(dir.absolutePath() + QDir::separator() + pluginPath); if (pluginLoader.isLoaded()) { qDebug() << "Plugin already loaded: " << pluginLoader.fileName(); continue; } QObject *pluginInstance = pluginLoader.instance(); // Check if the plugin loaded correctly. Keep debug output for now, should // go away once we have verified this (or added to a logger). if (!pluginInstance) { qDebug() << "Failed to load" << pluginPath << "error" << pluginLoader.errorString(); } else { qDebug() << "Loaded" << pluginPath << "->"; pluginInstance->dumpObjectInfo(); } // Now attempt to cast to known factory types, and make it available. ConnectionListenerFactory *connectionListenerFactory = qobject_cast(pluginInstance); if (connectionListenerFactory && !m_connectionListenerFactories.contains(connectionListenerFactory)) m_connectionListenerFactories.append(connectionListenerFactory); } } QList PluginManager::connectionListenerFactories() const { return m_connectionListenerFactories; } } // End MoleQueue namespace molequeue-0.9.0/molequeue/app/pluginmanager.h000066400000000000000000000047511323436134600213200ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef PLUGINMANAGER_H #define PLUGINMANAGER_H #include #include #include namespace MoleQueue { class ConnectionListenerFactory; /*! * \class PluginManager pluginmanager.h * \brief This class takes care of finding and loading MoleQueue plugins. * * This class will find and load MoleQueue plugins. Once loaded you can use an * instance of this class to query and construct plugin instances. By default * plugins are loaded from * QApplication::applicationDirPath()../lib/molequeue/plugins but this can be * changed or more paths can be added. */ class PluginManager : public QObject { Q_OBJECT public: /*! Get the singleton instance of the plugin manager. This instance should not * be deleted. */ static PluginManager * instance(); /*! Get a reference to the plugin directory path list. Modifying this before * calling load will allow you to add, remove or append to the search paths. */ QStringList& pluginDirList() { return m_pluginDirs; } /*! Load all plugins available in the specified plugin directories. */ void load(); void load(const QString &dir); /*! Return the loaded connection listener factories. Will be empty unless load was * already called. */ QList connectionListenerFactories() const; private: // Hide the constructor, destructor, copy and assignment operator. PluginManager(QObject *parent = 0); ~PluginManager(); PluginManager(const PluginManager&); // Not implemented. PluginManager& operator=(const PluginManager&); // Not implemented. QStringList m_pluginDirs; QString m_relativeToApp; // Various factories loaded by the plugin manager. QList m_connectionListenerFactories; }; } // End MoleQueue namespace #endif // PLUGINMANAGER_H molequeue-0.9.0/molequeue/app/program.cpp000066400000000000000000000163441323436134600204720ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "program.h" #include "logger.h" #include "queue.h" #include "queues/remote.h" #include "queuemanager.h" #include "server.h" #include #include #include namespace MoleQueue { Program::Program(Queue *parentQueue) : QObject(parentQueue), m_queue(parentQueue), m_queueManager((m_queue) ? m_queue->queueManager() : NULL), m_server((m_queueManager) ? m_queueManager->server() : NULL), m_name("Program"), m_executable("program"), m_arguments(), m_outputFilename("$$inputFileBaseName$$.out"), m_launchSyntax(REDIRECT), m_customLaunchTemplate("") { } Program::Program(const Program &other) : QObject(other.parent()), m_queue(other.m_queue), m_name(other.m_name), m_executable(other.m_executable), m_arguments(other.m_arguments), m_outputFilename(other.m_outputFilename), m_launchSyntax(other.m_launchSyntax), m_customLaunchTemplate(other.m_customLaunchTemplate) { } Program::~Program() { } Program &Program::operator=(const Program &other) { m_queue = other.m_queue; m_name = other.m_name; m_executable = other.m_executable; m_arguments = other.m_arguments; m_outputFilename = other.m_outputFilename; m_launchSyntax = other.m_launchSyntax; m_customLaunchTemplate = other.m_customLaunchTemplate; return *this; } QString Program::queueName() const { if (m_queue) return m_queue->name(); else return "None"; } bool Program::importSettings(const QString &fileName) { if (!QFile::exists(fileName)) return false; QFile stateFile(fileName); if (!stateFile.open(QFile::ReadOnly | QFile::Text)) { Logger::logError(tr("Cannot read program information from %1.") .arg(fileName)); return false; } QByteArray inputText = stateFile.readAll(); stateFile.close(); // Try to read existing data in QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(inputText, &error); if (error.error != QJsonParseError::NoError) { Logger::logError(tr("Error parsing program state from %1: %2\n%3") .arg(fileName) .arg(tr("%1 (at offset %2)") .arg(error.errorString()) .arg(error.offset)) .arg(inputText.data())); return false; } if (!doc.isObject()) { Logger::logError(tr("Error reading program state from %1: " "root is not an object!\n%2") .arg(fileName) .arg(inputText.data())); return false; } return readJsonSettings(doc.object(), true); } bool Program::exportSettings(const QString &fileName) const { QFile stateFile(fileName); if (!stateFile.open(QFile::ReadWrite | QFile::Text | QFile::Truncate)) { Logger::logError(tr("Cannot save program information for %1 in queue %2: " "Cannot open file %3.").arg(name()) .arg(m_queue->name()).arg(fileName)); return false; } QJsonObject root; if (!this->writeJsonSettings(root, true)) { stateFile.close(); return false; } // Write the data back out: stateFile.write(QJsonDocument(root).toJson()); stateFile.close(); return true; } bool Program::writeJsonSettings(QJsonObject &json, bool exportOnly) const { // No export sensitive data. Q_UNUSED(exportOnly) json.insert("executable", m_executable); json.insert("arguments", m_arguments); json.insert("outputFilename", m_outputFilename); json.insert("customLaunchTemplate", m_customLaunchTemplate); json.insert("launchSyntax", static_cast(m_launchSyntax)); return true; } bool Program::readJsonSettings(const QJsonObject &json, bool importOnly) { // No import sensitive data. Q_UNUSED(importOnly) // Validate JSON if (!json.value("executable").isString() || !json.value("arguments").isString() || !json.value("outputFilename").isString() || !json.value("customLaunchTemplate").isString() || !json.value("launchSyntax").isDouble()) { Logger::logError(tr("Error reading program config: Invalid format:\n%1") .arg(QString(QJsonDocument(json).toJson()))); return false; } m_executable = json.value("executable").toString(); m_arguments = json.value("arguments").toString(); m_outputFilename = json.value("outputFilename").toString(); m_customLaunchTemplate = json.value("customLaunchTemplate").toString(); m_launchSyntax = static_cast( static_cast(json.value("launchSyntax").toDouble() + 0.5)); return true; } QString Program::launchTemplate() const { if (m_launchSyntax == CUSTOM) return m_customLaunchTemplate; QString result = m_queue ? m_queue->launchTemplate() : QString("$$programExecution$$"); if (result.contains("$$programExecution$$")) { const QString progExec = Program::generateFormattedExecutionString( m_executable, m_arguments, m_outputFilename, m_launchSyntax); result.replace("$$programExecution$$", progExec); } if (QueueRemote *remoteQueue = qobject_cast(m_queue)) { if (result.contains("$$remoteWorkingDir$$")) { const QString remoteWorkingDir = QString("%1/%2/") .arg(remoteQueue->workingDirectoryBase()) .arg("$$moleQueueId$$"); result.replace("$$remoteWorkingDir$$", remoteWorkingDir); } } return result; } QString Program::generateFormattedExecutionString( const QString &executable_, const QString &arguments_, const QString &outputFilename_, Program::LaunchSyntax syntax_) { if (syntax_ == Program::CUSTOM) { return ""; } QString execStr; QString command = executable_ + (arguments_.isEmpty() ? QString() : " " + arguments_); switch (syntax_) { case Program::PLAIN: execStr += command; break; case Program::INPUT_ARG: execStr += QString("%1 $$inputFileName$$\n").arg(command); break; case Program::INPUT_ARG_NO_EXT: { execStr += QString("%1 $$inputFileBaseName$$\n").arg(command); } break; case Program::REDIRECT: execStr += QString("%1 < $$inputFileName$$ > %3\n") .arg(command).arg(outputFilename_); break; case Program::INPUT_ARG_OUTPUT_REDIRECT: execStr += QString("%1 $$inputFileName$$ > %3\n") .arg(command).arg(outputFilename_); break; case Program::CUSTOM: // Should be handled as a special case earlier. execStr = tr("Internal MoleQueue error: Custom syntax type not handled.\n"); break; default: execStr = tr("Internal MoleQueue error: Unrecognized syntax type.\n"); break; } return execStr; } } // End namespace molequeue-0.9.0/molequeue/app/program.h000066400000000000000000000144161323436134600201350ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef PROGRAM_H #define PROGRAM_H #include #include #include #include class QJsonObject; namespace MoleQueue { class Queue; class QueueManager; class Server; /** * @class Program program.h * @brief A class defining interactions with an executable accessible by a Queue. * @author Marcus D. Hanwell, David C. Lonie * * The Program class describes an executable which runs a Job on a particular * Queue. Each Program object is unique to the Queue, and contains details * for running the executable, any arguments it needs, and the names of files * it reads/produces. */ class Program : public QObject { Q_OBJECT public: explicit Program(Queue *parentQueue = 0); Program(const Program &other); ~Program(); Program &operator=(const Program &other); /// Enum used for various common styles of execution syntax enum LaunchSyntax { /// Use custom launch script CUSTOM = 0, /// Only run the executable, e.g. "vasp" PLAIN, /// Single argument is the name of the input file with extension, e.g. /// "mopac job.mop" INPUT_ARG, /// Single argument is the name of the input file without extension, e.g. /// "mopac job" INPUT_ARG_NO_EXT, /// Redirect input file to stdin and stdout to output file, e.g. /// "gulp < job.gin > job.got" REDIRECT, /// Input as argument, redirect stdout to output file, e.g. /// "gamess job.inp > job.out" INPUT_ARG_OUTPUT_REDIRECT, /// Use to get total number of syntax types. SYNTAX_COUNT }; /// @return The parent Server Server *server() {return m_server;} /// @return The parent Server const Server *server() const {return m_server;} /// @return The parent QueueManager QueueManager *queueManager() {return m_queueManager;} /// @return The parent Server const QueueManager *queueManager() const {return m_queueManager;} /// @return The Queue that this Program belongs to. Queue * queue() { return m_queue; } /// @return The Queue that this Program belongs to. const Queue * queue() const { return m_queue; } /// @return The name of the Queue that this Program belongs to. QString queueName() const; /// Import the program's configuration from the indicated file (.mqp format) bool importSettings(const QString &fileName); /// Export the program's configuration into the indicated file (.mqp format) bool exportSettings(const QString &fileName) const; /** * @brief writeJsonSettings Write the program's internal state into a JSON * object. * @param value Target JSON object. * @param exportOnly If true, instance specific information (e.g. system * specific paths, etc) is omitted. * @return True on success, false on failure. */ bool writeJsonSettings(QJsonObject &json, bool exportOnly) const; /** * @brief readJsonSettings Initialize the program's internal state from a JSON * object. * @param value Source JSON object. * @param importOnly If true, instance specific information (e.g. system * specific paths, etc) is ignored. * @return True on success, false on failure. */ bool readJsonSettings(const QJsonObject &json, bool importOnly); /** * Set the name of the program. This is the name that will show up in * the GUI, and many common names such as GAMESS, GAMESS-UK, Gaussian, * MolPro etc are used by GUIs such as Avogadro with its input generator * dialogs to match up input files to programs. * @param newName Name to use in GUIs */ void setName(const QString &newName) { if (newName != m_name) { QString oldName = m_name; m_name = newName; emit nameChanged(newName, oldName); } } /// @return The name of the program. Often used by GUIs etc. QString name() const { return m_name; } void setExecutable(const QString &str) {m_executable = str;} QString executable() const {return m_executable;} void setArguments(const QString &str) {m_arguments = str;} QString arguments() const {return m_arguments;} void setOutputFilename(const QString &str) {m_outputFilename = str;} QString outputFilename() const {return m_outputFilename;} void setLaunchSyntax(LaunchSyntax s) { if (s >= SYNTAX_COUNT) return; m_launchSyntax = s; } LaunchSyntax launchSyntax() const {return m_launchSyntax;} void setCustomLaunchTemplate(const QString &str) { m_customLaunchTemplate = str; } QString customLaunchTemplate() const {return m_customLaunchTemplate;} /// @return Either the custom launch template or a default generated template, /// depending on the value of launchSyntax. QString launchTemplate() const; static QString generateFormattedExecutionString( const QString &executable_, const QString &arguments_, const QString &outputFilename_, LaunchSyntax syntax_); signals: /** * Emitted when the name of the program is changed. */ void nameChanged(const QString &newName, const QString &oldName); protected: /// The Queue that the Program belongs to/is being run by. Queue *m_queue; /// The QueueManager owning the Queue this Program belongs to. QueueManager *m_queueManager; /// The Server this program is associated with. Server *m_server; /// GUI-visible name QString m_name; /// Name of executable QString m_executable; /// Executable arguments QString m_arguments; /// Output filename QString m_outputFilename; /// Launch syntax style LaunchSyntax m_launchSyntax; /// Bash/Shell/Queue script template used to launch program QString m_customLaunchTemplate; }; } // End namespace Q_DECLARE_METATYPE(MoleQueue::Program*) Q_DECLARE_METATYPE(const MoleQueue::Program*) #endif // PROGRAM_H molequeue-0.9.0/molequeue/app/programconfiguredialog.cpp000066400000000000000000000267661323436134600235650ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "programconfiguredialog.h" #include "ui_programconfiguredialog.h" #include "filebrowsewidget.h" #include "program.h" #include "queue.h" #include "queues/local.h" #include "templatekeyworddialog.h" #include #include #include #include #include #include #include #include namespace MoleQueue { ProgramConfigureDialog::ProgramConfigureDialog(Program *program, QWidget *parentObject) : QDialog(parentObject), ui(new Ui::ProgramConfigureDialog), m_program(program), m_helpDialog(NULL), m_isCustomized((m_program->launchSyntax() == Program::CUSTOM)), m_dirty(true), m_isLocal((m_program != NULL && qobject_cast(m_program->queue()) != NULL)) { ui->setupUi(this); setExecutableWidget(); populateSyntaxCombo(); connect(ui->combo_syntax, SIGNAL(currentIndexChanged(int)), this, SLOT(launchSyntaxChanged(int))); connect(ui->push_customize, SIGNAL(clicked()), this, SLOT(customizeLauncherClicked())); connect(ui->edit_arguments, SIGNAL(textChanged(QString)), this, SLOT(updateLaunchEditor())); connect(ui->edit_outputFilename, SIGNAL(textChanged(QString)), this, SLOT(updateLaunchEditor())); connect(ui->text_launchTemplate, SIGNAL(textChanged()), this, SLOT(launchEditorTextChanged())); connect(ui->templateHelpButton, SIGNAL(clicked()), this, SLOT(showHelpDialog())); connect(ui->edit_name, SIGNAL(textChanged(QString)), this, SLOT(setDirty())); connect(ui->edit_arguments, SIGNAL(textChanged(QString)), this, SLOT(setDirty())); connect(ui->edit_outputFilename, SIGNAL(textChanged(QString)), this, SLOT(setDirty())); connect(ui->combo_syntax, SIGNAL(currentIndexChanged(int)), this, SLOT(setDirty())); connect(ui->push_customize, SIGNAL(clicked()), this, SLOT(setDirty())); connect(ui->text_launchTemplate, SIGNAL(textChanged()), this, SLOT(setDirty())); connect(ui->buttonBox, SIGNAL(clicked(QAbstractButton*)), this, SLOT(buttonBoxButtonClicked(QAbstractButton*))); updateGuiFromProgram(); launchSyntaxChanged(ui->combo_syntax->currentIndex()); ui->edit_name->setValidator(new QRegExpValidator( QRegExp(VALID_NAME_REG_EXP))); setDirty(false); } ProgramConfigureDialog::~ProgramConfigureDialog() { delete ui; } void ProgramConfigureDialog::accept() { if (m_dirty) if (!updateProgramFromGui()) return; QDialog::accept(); } void ProgramConfigureDialog::populateSyntaxCombo() { QStringList syntaxList; for (int cur = 0; cur < static_cast(Program::SYNTAX_COUNT); ++cur) { switch (static_cast(cur)) { case Program::CUSTOM: syntaxList << tr("Custom"); break; case Program::PLAIN: syntaxList << tr("Plain"); break; case Program::INPUT_ARG: syntaxList << tr("Input as argument"); break; case Program::INPUT_ARG_NO_EXT: syntaxList << tr("Input as argument (no extension)"); break; case Program::REDIRECT: syntaxList << tr("Redirect input and output"); break; case Program::INPUT_ARG_OUTPUT_REDIRECT: syntaxList << tr("Input as argument, redirect output"); break; case Program::SYNTAX_COUNT: default: continue; } } ui->combo_syntax->blockSignals(true); ui->combo_syntax->clear(); ui->combo_syntax->addItems(syntaxList); ui->combo_syntax->blockSignals(false); } void ProgramConfigureDialog::updateGuiFromProgram() { ui->edit_name->setText(m_program->name()); setExecutableName(m_program->executable()); ui->edit_arguments->setText(m_program->arguments()); ui->edit_outputFilename->setText(m_program->outputFilename()); Program::LaunchSyntax syntax = m_program->launchSyntax(); ui->combo_syntax->blockSignals(true); ui->combo_syntax->setCurrentIndex(static_cast(syntax)); ui->combo_syntax->blockSignals(false); m_customLaunchText = m_program->customLaunchTemplate(); updateLaunchEditor(); setDirty(false); } bool ProgramConfigureDialog::updateProgramFromGui() { // If the name changed, check that it won't collide with an existing program. QString name = ui->edit_name->text().trimmed(); if (name != m_program->name()) { if (Queue *queue = m_program->queue()) { if (queue->programNames().contains(name)) { int reply = QMessageBox::warning(this, tr("Name conflict"), tr("The program name has been changed to '%1'," " but there is already a program with that " "name.\n\nOverwrite existing program?") .arg(name), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (reply != QMessageBox::Yes) { ui->edit_name->selectAll(); ui->edit_name->setFocus(); return false; } } m_program->setName(name); } } m_program->setExecutable(executableName()); m_program->setArguments(ui->edit_arguments->text()); m_program->setOutputFilename(ui->edit_outputFilename->text()); Program::LaunchSyntax syntax = static_cast( ui->combo_syntax->currentIndex()); m_program->setLaunchSyntax(syntax); m_program->setCustomLaunchTemplate(m_customLaunchText); setDirty(false); return true; } void ProgramConfigureDialog::updateLaunchEditor() { Program::LaunchSyntax syntax = static_cast( ui->combo_syntax->currentIndex()); if (syntax == Program::CUSTOM) { ui->text_launchTemplate->document()->setPlainText(m_customLaunchText); return; } QString launchText; if (!m_program->queue() || (qobject_cast(m_program->queue()) && syntax != Program::CUSTOM)) { launchText = "$$programExecution$$\n"; } else { launchText = m_program->queue()->launchTemplate(); } const QString executable = executableName(); const QString arguments = ui->edit_arguments->text(); const QString outputFilename = ui->edit_outputFilename->text(); QString programExecution = Program::generateFormattedExecutionString( executable, arguments, outputFilename, syntax); launchText.replace("$$programExecution$$", programExecution); ui->text_launchTemplate->document()->setPlainText(launchText); } void ProgramConfigureDialog::launchEditorTextChanged() { QString launchText = ui->text_launchTemplate->document()->toPlainText(); Program::LaunchSyntax syntax = static_cast( ui->combo_syntax->currentIndex()); if (syntax == Program::CUSTOM) m_customLaunchText = launchText; /// @todo Syntax highlighting? } void ProgramConfigureDialog::launchSyntaxChanged(int enumVal) { Q_UNUSED(enumVal); Program::LaunchSyntax syntax = static_cast(enumVal); bool syntaxIsCustom = (syntax == Program::CUSTOM); ui->push_customize->setDisabled(syntaxIsCustom); ui->text_launchTemplate->setReadOnly(!syntaxIsCustom); updateLaunchEditor(); } void ProgramConfigureDialog::customizeLauncherClicked() { Program::LaunchSyntax syntax = static_cast( ui->combo_syntax->currentIndex()); if (qobject_cast(m_program->queue()) && syntax != Program::CUSTOM) { m_customLaunchText = m_program->queue()->launchTemplate(); QString execStr = ui->text_launchTemplate->document()->toPlainText(); m_customLaunchText.replace("$$programExecution$$", execStr); } else { m_customLaunchText = ui->text_launchTemplate->document()->toPlainText(); } ui->combo_syntax->setCurrentIndex(static_cast(Program::CUSTOM)); } void ProgramConfigureDialog::setDirty(bool dirty) { if (dirty != m_dirty) { m_dirty = dirty; ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(m_dirty); } } void ProgramConfigureDialog::closeEvent(QCloseEvent *e) { if (m_dirty) { // apply or discard changes? QMessageBox::StandardButton reply = QMessageBox::warning(this, tr("Unsaved changes"), tr("The changes to the program have not been " "saved. Would you like to save or discard " "them?"), QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Save); switch (reply) { case QMessageBox::Cancel: e->ignore(); return; case QMessageBox::Save: if (!updateProgramFromGui()) return; case QMessageBox::NoButton: case QMessageBox::Discard: default: e->accept(); break; } } QDialog::closeEvent(e); } void ProgramConfigureDialog::keyPressEvent(QKeyEvent *e) { // By default, the escape key bypasses the close event, but we still want to // check if the settings widget is dirty. if (e->key() == Qt::Key_Escape) { e->accept(); close(); return; } QDialog::keyPressEvent(e); } void ProgramConfigureDialog::setExecutableWidget() { // Allow local file browsing if the program is used with a local queue. if (m_isLocal) { ui->label_executable->setBuddy(ui->browse_localExecutable); ui->browse_localExecutable->setMode(FileBrowseWidget::ExecutableFile); connect(ui->browse_localExecutable, SIGNAL(fileNameChanged(QString)), SLOT(updateLaunchEditor())); connect(ui->browse_localExecutable, SIGNAL(fileNameChanged(QString)), SLOT(setDirty())); } else { // Just use a line edit if the queue is remote. ui->label_executable->setBuddy(ui->edit_remoteExecutable); connect(ui->edit_remoteExecutable, SIGNAL(textChanged(QString)), SLOT(updateLaunchEditor())); connect(ui->edit_remoteExecutable, SIGNAL(textChanged(QString)), SLOT(setDirty())); } ui->browse_localExecutable->setVisible(m_isLocal); ui->edit_remoteExecutable->setVisible(!m_isLocal); } QString ProgramConfigureDialog::executableName() const { return m_isLocal ? ui->browse_localExecutable->fileName() : ui->edit_remoteExecutable->text(); } void ProgramConfigureDialog::setExecutableName(const QString &name) { if (m_isLocal) ui->browse_localExecutable->setFileName(name); else ui->edit_remoteExecutable->setText(name); } void ProgramConfigureDialog::showHelpDialog() { if (!m_helpDialog) m_helpDialog = new TemplateKeywordDialog(this); m_helpDialog->show(); } void ProgramConfigureDialog::buttonBoxButtonClicked(QAbstractButton *button) { // "Ok" and "Cancel" are directly connected to accept() and reject(), so only // check for "apply" here: if (button == ui->buttonBox->button(QDialogButtonBox::Apply)) updateProgramFromGui(); } } // end namespace MoleQueue molequeue-0.9.0/molequeue/app/programconfiguredialog.h000066400000000000000000000037461323436134600232230ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef PROGRAMCONFIGUREDIALOG_H #define PROGRAMCONFIGUREDIALOG_H #include class QAbstractButton; namespace Ui { class ProgramConfigureDialog; } namespace MoleQueue { class Program; class TemplateKeywordDialog; /// @brief Dialog for setting Program configuration options. class ProgramConfigureDialog : public QDialog { Q_OBJECT public: explicit ProgramConfigureDialog(Program *program, QWidget *parentObject = 0); ~ProgramConfigureDialog(); Program *currentProgram() const { return m_program; } public slots: void accept(); protected slots: void populateSyntaxCombo(); void updateGuiFromProgram(); bool updateProgramFromGui(); void updateLaunchEditor(); void launchEditorTextChanged(); void launchSyntaxChanged(int enumVal); void customizeLauncherClicked(); void setDirty(bool dirty = true); void showHelpDialog(); void buttonBoxButtonClicked(QAbstractButton*); protected: void closeEvent(QCloseEvent *); void keyPressEvent(QKeyEvent *); private: void setExecutableWidget(); QString executableName() const; void setExecutableName(const QString &name); Ui::ProgramConfigureDialog *ui; Program *m_program; TemplateKeywordDialog *m_helpDialog; bool m_isCustomized; bool m_dirty; bool m_isLocal; QString m_customLaunchText; }; } // end namespace MoleQueue #endif // PROGRAMCONFIGUREDIALOG_H molequeue-0.9.0/molequeue/app/puttycommand.cpp000066400000000000000000000025671323436134600215510ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "puttycommand.h" namespace MoleQueue { PuttyCommand::PuttyCommand(QObject *parentObject) : SshCommand(parentObject, "plink", "pscp") { } PuttyCommand::~PuttyCommand() { } QStringList PuttyCommand::sshArgs() { QStringList args; if (!m_identityFile.isEmpty()) args << "-i" << m_identityFile; if (m_portNumber >= 0 && m_portNumber != 22) args << "-p" << QString::number(m_portNumber); return args; } QStringList PuttyCommand::scpArgs() { QStringList args; if (!m_identityFile.isEmpty()) args << "-i" << m_identityFile; if (m_portNumber >= 0 && m_portNumber != 22) args << "-P" << QString::number(m_portNumber); return args; } } // End namespace molequeue-0.9.0/molequeue/app/puttycommand.h000066400000000000000000000030631323436134600212060ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef PUTTYCOMMAND_H #define PUTTYCOMMAND_H #include "sshcommand.h" namespace MoleQueue { class TerminalProcess; /** * @class PuttyCommand puttycommand.h * @brief Concrete implementation of SshCommand using commandline plink/pscp. * @author Marcus D. Hanwell, David C. Lonie, Chris Harris * * The PuttyCommand provides an implementation of the SshCommand interface * that calls the commandline plink and pscp executables in a TerminalProcess. * * When writing code that needs ssh functionality, the code should use the * SshConnection interface instead. */ class PuttyCommand : public SshCommand { Q_OBJECT public: PuttyCommand(QObject *parentObject = 0); ~PuttyCommand(); protected: /// @return the arguments to be passed to the SSH command. QStringList sshArgs(); /// @return the arguments to be passed to the SCP command. QStringList scpArgs(); }; } // End namespace #endif molequeue-0.9.0/molequeue/app/queue.cpp000066400000000000000000000425271323436134600201510ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "queue.h" #include "filespecification.h" #include "filesystemtools.h" #include "job.h" #include "jobmanager.h" #include "logentry.h" #include "logger.h" #include "program.h" #include "queues/local.h" #include "queues/remote.h" #include "queuemanager.h" #include "server.h" #include #include #include namespace MoleQueue { Queue::Queue(const QString &queueName, QueueManager *parentManager) : QObject(parentManager), m_queueManager(parentManager), m_server((m_queueManager) ? m_queueManager->server() : NULL), m_name(queueName) { qRegisterMetaType("MoleQueue::Program*"); qRegisterMetaType("const MoleQueue::Program*"); qRegisterMetaType("MoleQueue::IdType"); qRegisterMetaType("MoleQueue::JobState"); if (m_server) { connect(m_server->jobManager(), SIGNAL(jobAboutToBeRemoved(const MoleQueue::Job&)), this, SLOT(jobAboutToBeRemoved(const MoleQueue::Job&))); } } Queue::~Queue() { QList programList = m_programs.values(); m_programs.clear(); qDeleteAll(programList); } bool Queue::readSettings(const QString &stateFilename) { return readJsonSettingsFromFile(stateFilename, false, true); } bool Queue::writeSettings() const { QString fileName = stateFileName(); if (fileName.isEmpty()) { Logger::logError(tr("Cannot write settings for Queue %1: Cannot determine " "config filename.").arg(name())); return false; } // Create directory if needed. QDir queueDir(QFileInfo(fileName).dir()); if (!queueDir.exists()) { if (!queueDir.mkpath(queueDir.absolutePath())) { Logger::logError(tr("Cannot write settings for Queue %1: Cannot create " "config directory %2.").arg(name()) .arg(queueDir.absolutePath())); return false; } } return writeJsonSettingsToFile(fileName, false, true); } bool Queue::exportSettings(const QString &fileName, bool includePrograms) const { return writeJsonSettingsToFile(fileName, true, includePrograms); } bool Queue::importSettings(const QString &fileName, bool includePrograms) { return readJsonSettingsFromFile(fileName, true, includePrograms); } QString Queue::queueTypeFromFile(const QString &mqqFile) { QString result; if (!QFile::exists(mqqFile)) return result; QFile stateFile(mqqFile); if (!stateFile.open(QFile::ReadOnly | QFile::Text)) return result; QByteArray inputText = stateFile.readAll(); stateFile.close(); // Try to read existing data in QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(inputText, &error); if (error.error != QJsonParseError::NoError || !doc.isObject()) return result; if (doc.object().value("type").isString()) result = doc.object().value("type").toString(); return result; } QString Queue::stateFileName() const { QString workDir; if (m_queueManager) { workDir = m_queueManager->queueConfigDirectory(); } if (workDir.isEmpty()) { Logger::logError(tr("Cannot determine stateFileName for queue '%1'")); return ""; } return QDir::cleanPath(workDir + "/" + name() + ".mqq"); } bool Queue::writeJsonSettingsToFile(const QString &stateFilename, bool exportOnly, bool includePrograms) const { QFile stateFile(stateFilename); if (!stateFile.open(QFile::ReadWrite | QFile::Text | QFile::Truncate)) { Logger::logError(tr("Cannot save queue information for queue %1 in %2: " "Cannot open file.").arg(name()).arg(stateFilename)); return false; } QJsonObject root; if (!this->writeJsonSettings(root, exportOnly, includePrograms)) { stateFile.close(); return false; } // Write the data back out: stateFile.write(QJsonDocument(root).toJson()); stateFile.close(); return true; } bool Queue::readJsonSettingsFromFile(const QString &stateFilename, bool importOnly, bool includePrograms) { if (!QFile::exists(stateFilename)) return false; QFile stateFile(stateFilename); if (!stateFile.open(QFile::ReadOnly | QFile::Text)) { Logger::logError(tr("Cannot read queue information from %1.") .arg(stateFilename)); return false; } // Try to read existing data in QByteArray inputText = stateFile.readAll(); QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(inputText, &error); if (error.error != QJsonParseError::NoError) { Logger::logError(tr("Error parsing queue state from %1: %2\n%3") .arg(stateFilename) .arg(tr("%1 (at offset %2)") .arg(error.errorString()) .arg(error.offset)) .arg(inputText.data())); stateFile.close(); return false; } if (!doc.isObject()) { Logger::logError(tr("Error reading queue state from %1: " "root is not an object!\n%2") .arg(stateFilename) .arg(inputText.data())); stateFile.close(); return false; } return readJsonSettings(doc.object(), importOnly, includePrograms); } bool Queue::writeJsonSettings(QJsonObject &root, bool exportOnly, bool includePrograms) const { root.insert("type", typeName()); root.insert("launchTemplate", m_launchTemplate); root.insert("launchScriptName", m_launchScriptName); if (!exportOnly) { QJsonObject jobIdMap; foreach (IdType key, m_jobs.keys()) jobIdMap.insert(idTypeToString(key), idTypeToJson(m_jobs[key])); root.insert("jobIdMap", jobIdMap); } if (includePrograms) { QJsonObject programsObject; foreach (const Program *prog, programs()) { QJsonObject programObject; if (prog->writeJsonSettings(programObject, exportOnly)) { programsObject.insert(prog->name(), programObject); } else { Logger::logError(tr("Could not save program %1 in queue %2's settings.") .arg(prog->name(), name())); } } root.insert("programs", programsObject); } return true; } bool Queue::readJsonSettings(const QJsonObject &root, bool importOnly, bool includePrograms) { // Verify JSON: if (!root.value("type").isString() || !root.value("launchTemplate").isString() || !root.value("launchScriptName").isString() || (root.contains("programs") && !root.value("programs").isObject())) { Logger::logError(tr("Error reading queue settings: Invalid format:\n%1") .arg(QString(QJsonDocument(root).toJson()))); return false; } if (typeName() != root.value("type").toString()) { Logger::logError(tr("Error reading queue settings: Types do not match.\n" "Expected %1, got %2.").arg(typeName()) .arg(root.value("type").toString())); return false; } QMap jobIdMap; if (!importOnly && root.contains("jobIdMap")) { if (!root.value("jobIdMap").isObject()) { Logger::logError(tr("Error reading queue settings: Invalid format:\n%1") .arg(QString(QJsonDocument(root).toJson()))); return false; } QJsonObject jobIdObject = root.value("jobIdMap").toObject(); foreach (const QString &key, jobIdObject.keys()) { IdType jobId = toIdType(key); IdType moleQueueId = toIdType(jobIdObject.value(key)); jobIdMap.insert(jobId, moleQueueId); } } QMap programMap; if (includePrograms && root.contains("programs")) { if (!root.value("programs").isObject()) { Logger::logError(tr("Error reading queue settings: Invalid format:\n%1") .arg(QString(QJsonDocument(root).toJson()))); return false; } QJsonObject programObject = root.value("programs").toObject(); foreach (const QString &progName, programObject.keys()) { if (!programObject.value(progName).isObject()) { Logger::logError(tr("Error loading configuration for program %1 in " "queue %2.").arg(progName).arg(name())); qDeleteAll(programMap.values()); return false; } Program *prog = new Program(this); programMap.insert(progName, prog); prog->setName(progName); if (!prog->readJsonSettings(programObject.value(progName).toObject(), importOnly)) { Logger::logError(tr("Error loading configuration for program %1 in " "queue %2.").arg(progName).arg(name())); qDeleteAll(programMap.values()); return false; } } } // Everything is verified -- go ahead and update queue. m_launchTemplate = root.value("launchTemplate").toString(); m_launchScriptName = root.value("launchScriptName").toString(); if (!importOnly) m_jobs = jobIdMap; if (includePrograms) { for (QMap::const_iterator it = programMap.constBegin(), it_end = programMap.constEnd(); it != it_end; ++it) { if (!addProgram(it.value())) { Logger::logDebugMessage(tr("Cannot add program '%1' to queue '%2': " "program name already exists!") .arg(it.key()).arg(name())); it.value()->deleteLater(); continue; } } } return true; } AbstractQueueSettingsWidget* Queue::settingsWidget() { return NULL; } bool Queue::addProgram(Program *newProgram, bool replace) { // Check for duplicates, unless we are replacing, and return false if found. if (m_programs.contains(newProgram->name())) { if (replace) m_programs.take(newProgram->name())->deleteLater(); else return false; } connect(newProgram, SIGNAL(nameChanged(QString,QString)), this, SLOT(programNameChanged(QString,QString))); m_programs.insert(newProgram->name(), newProgram); if (newProgram->parent() != this) newProgram->setParent(this); emit programAdded(newProgram->name(), newProgram); return true; } bool Queue::removeProgram(Program* programToRemove) { return removeProgram(programToRemove->name()); } bool Queue::removeProgram(const QString &programName) { if (!m_programs.contains(programName)) return false; Program *program = m_programs.take(programName); emit programRemoved(programName, program); return true; } void Queue::replaceKeywords(QString &launchScript, const Job &job, bool addNewline) { if (job.isValid()) { if (Program *program = lookupProgram(job.program())) { // This will probably contain other keywords (like inputFileBaseName), so // keep it towards the top of the replacement list. launchScript.replace("$$outputFileName$$", program->outputFilename()); } job.replaceKeywords(launchScript); } // Remove any unreplaced keywords QRegExp expr("[^\\$]?(\\${2,3}[^\\$\\s]+\\${2,3})[^\\$]?"); while (expr.indexIn(launchScript) != -1) { Logger::logWarning(tr("Unhandled keyword in launch script: %1. Removing.") .arg(expr.cap(1)), job.moleQueueId()); launchScript.remove(expr.cap(1)); } // Add newline at end if not present if (addNewline && !launchScript.isEmpty() && !launchScript.endsWith(QChar('\n'))) { launchScript.append(QChar('\n')); } } bool Queue::writeInputFiles(const Job &job) { QString workdir = job.localWorkingDirectory(); // Lookup program. if (!m_server) { Logger::logError(tr("Queue '%1' cannot locate Server instance!") .arg(m_name), job.moleQueueId()); return false; } const Program *program = lookupProgram(job.program()); if (!program) { Logger::logError(tr("Queue '%1' cannot locate program '%2'!") .arg(m_name).arg(job.program()), job.moleQueueId()); return false; } // Create directory QDir dir (workdir); /// Send a warning but don't bail if the path already exists. if (dir.exists()) { Logger::logWarning(tr("Directory already exists: %1") .arg(dir.absolutePath()), job.moleQueueId()); } else { if (!dir.mkpath(dir.absolutePath())) { Logger::logError(tr("Cannot create directory: %1") .arg(dir.absolutePath()), job.moleQueueId()); return false; } } // Create input files FileSpecification inputFile = job.inputFile(); if (inputFile.isValid()) inputFile.writeFile(dir); // Write additional input files QList additionalInputFiles = job.additionalInputFiles(); foreach (const FileSpecification &filespec, additionalInputFiles) { if (!filespec.isValid()) { Logger::logError(tr("Writing additional input files...invalid FileSpec:\n" "%1").arg(QString(filespec.toJson())), job.moleQueueId()); return false; } QFileInfo target(dir.absoluteFilePath(filespec.filename())); switch (filespec.format()) { default: case FileSpecification::InvalidFileSpecification: Logger::logWarning(tr("Cannot write input file. Invalid filespec:\n%1") .arg(QString(filespec.toJson())), job.moleQueueId()); continue; case FileSpecification::PathFileSpecification: { QFileInfo source(filespec.filepath()); if (!source.exists()) { Logger::logError(tr("Writing additional input files...Source file " "does not exist! %1") .arg(source.absoluteFilePath()), job.moleQueueId()); return false; } if (source == target) { Logger::logWarning(tr("Refusing to copy additional input file...source " "and target refer to the same file!\nSource: %1" "\nTarget: %2").arg(source.absoluteFilePath()) .arg(target.absoluteFilePath()), job.moleQueueId()); continue; } } case FileSpecification::ContentsFileSpecification: if (target.exists()) { Logger::logWarning(tr("Writing additional input files...Overwriting " "existing file: '%1'") .arg(target.absoluteFilePath()), job.moleQueueId()); QFile::remove(target.absoluteFilePath()); } filespec.writeFile(dir); continue; } } // Do we need a driver script? const QueueLocal *localQueue = qobject_cast(this); const QueueRemote *remoteQueue = qobject_cast(this); if ((localQueue && program->launchSyntax() == Program::CUSTOM) || remoteQueue) { QFile launcherFile (dir.absoluteFilePath(launchScriptName())); if (!launcherFile.open(QFile::WriteOnly | QFile::Text)) { Logger::logError(tr("Cannot open file for writing: %1.") .arg(launcherFile.fileName()), job.moleQueueId()); return false; } QString launchString = program->launchTemplate(); replaceKeywords(launchString, job); launcherFile.write(launchString.toLatin1()); if (!launcherFile.setPermissions( launcherFile.permissions() | QFile::ExeUser)) { Logger::logError(tr("Cannot set executable permissions on file: %1.") .arg(launcherFile.fileName()), job.moleQueueId()); return false; } launcherFile.close(); } return true; } bool Queue::addJobFailure(IdType moleQueueId) { if (!m_failureTracker.contains(moleQueueId)) { m_failureTracker.insert(moleQueueId, 1); return true; } int failures = ++m_failureTracker[moleQueueId]; if (failures > 3) { Logger::logError(tr("Maximum number of retries for job %1 exceeded.") .arg(idTypeToString(moleQueueId)), moleQueueId); clearJobFailures(moleQueueId); return false; } return true; } void Queue::jobAboutToBeRemoved(const Job &job) { m_failureTracker.remove(job.moleQueueId()); m_jobs.remove(job.queueId()); } void Queue::programNameChanged(const QString &newName, const QString &oldName) { if (Program *prog = m_programs.value(oldName, NULL)) { if (prog->name() == newName) { // Reset the program map. m_programs.remove(oldName); m_programs.insert(newName, prog); // Update the configuration file. this->writeSettings(); emit programRenamed(newName, prog, oldName); } } } void Queue::cleanLocalDirectory(const Job &job) { if (!FileSystemTools::recursiveRemoveDirectory(job.localWorkingDirectory(), true)) { Logger::logError(tr("Cannot remove '%1' from local filesystem.") .arg(job.localWorkingDirectory())); } } } // End namespace molequeue-0.9.0/molequeue/app/queue.h000066400000000000000000000336771323436134600176240ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_QUEUE_H #define MOLEQUEUE_QUEUE_H #include #include "job.h" #include "idtypeutils.h" #include #include #include #include #include class QJsonObject; namespace MoleQueue { class AbstractQueueSettingsWidget; class Job; class Program; class QueueManager; class Server; /** * @class Queue queue.h * @brief Abstract interface for queuing systems. * @author Marcus D. Hanwell, David C. Lonie * * The Queue interface defines interactions with a distributed resource * management system, such as job submission and job status updates. Each * Queue object manages a set of Program instances, which contain information * about the related task of actually running an executable to do work. */ class Queue : public QObject { Q_OBJECT protected: /** * Protected constructor. Use QueueManager:::addQueue() method to create new * Queue objects. */ explicit Queue(const QString &queueName = "Undefined", QueueManager *parentManager = 0); public: ~Queue(); /// @return The parent Server Server *server() { return m_server; } /// @return The parent Server const Server *server() const { return m_server; } /// @return The parent QueueManager QueueManager *queueManager() { return m_queueManager; } /// @return The parent Server const QueueManager *queueManager() const { return m_queueManager; } /** * Set the name of the queue. This should be unique, and will be used in the * GUI to refer to this queue. */ virtual void setName(const QString &newName) { if (newName != m_name) { QString oldName = m_name; m_name = newName; emit nameChanged(newName, oldName); } } /** Get the name of the queue. */ QString name() const { return m_name; } /** * Returns the type of the queue as a string. */ virtual QString typeName() const { return "Unknown"; } /** * Read settings for the queue, done early on at startup. * @param filePath Path to the .mqq file with the queue settings. * * This method calls readJsonSettings to perform the actual setting of * state information. Subclasses should reimplement that method. */ bool readSettings(const QString &filePath); /** * Write settings for the queue, done just before closing the server. * The settings are written to * [server's local working directory]/config/queues/[queuename].mqq * * This method calls writeJsonSettings to perform the actual collection of * state information. Subclasses should reimplement that method. */ bool writeSettings() const; /** * Write this Queue's configuration to the .mqq file @a fileName. * Sensitive data (such as usernames, etc) and mutatable state data (like * current jobs) are not written, see writeSettings() if these are needed. * @param includePrograms Export this queue's programs as well. Default: true * * This method calls writeJsonSettings to perform the actual collection of * state information. Subclasses should reimplement that method. */ bool exportSettings(const QString &fileName, bool includePrograms = true) const; /** * Set this Queue's configuration using the the .mqq file @a fileName. * Sensitive data (such as usernames, etc) and mutatable state data (like * current jobs) are not read, see readSettings() if these are needed. * @param includePrograms Import any programs contained in the importer. * Default: true * * This method calls readJsonSettings to perform the actual setting of * state information. Subclasses should reimplement that method. */ bool importSettings(const QString &fileName, bool includePrograms = true); /** * @param mqqFile Filename of the mqq (MoleQueue Queue) file. * @return Return the type of queue that is stored in the .mqq file. */ static QString queueTypeFromFile(const QString &mqqFile); /** * @return The name of the persistant state file used to store this Queue's * configuration. */ QString stateFileName() const; /** * @brief writeJsonSettings Write the queue's internal state into a JSON * object. * @param value Target JSON object. * @param exportOnly If true, instance specific information (e.g. currently * running jobs, login details, etc) is omitted. * @param includePrograms Whether or not to include the Queue's program * configurations. * @return True on success, false on failure. */ virtual bool writeJsonSettings(QJsonObject &value, bool exportOnly, bool includePrograms) const; /** * @brief readJsonSettings Initialize the queue's internal state from a JSON * object. * @param value Source JSON object. * @param importOnly If true, instance specific information (e.g. currently * running jobs, login details, etc) is ignored. * @param includePrograms Whether or not to include the Queue's program * configurations. * @return True on success, false on failure. * * @note When reimplementing this method, verify and parse the Json object * into temporary variables, then call the base class implementation and only * modify the queue if the call returns true. */ virtual bool readJsonSettings(const QJsonObject &value, bool importOnly, bool includePrograms); /** * Returns a widget that can be used to configure the settings for the * queue. */ virtual AbstractQueueSettingsWidget* settingsWidget(); /** * Add a new program to the queue. Program names must be unique in each * queue, as they are used to specify which program will be used. * @note This Queue instance will take ownership and reparent @a newProgram. * @param program The program to be added to the queue. * @param replace Defaults to false, if true replace any program with the * same name in this queue. * @return True on success, false on failure. */ bool addProgram(Program *newProgram, bool replace = false); /** * Attempt to remove a program from the queue. The program name is used * as the criteria to decice which object to remove. * @param program The program to be removed from the queue. * @return True on success, false on failure. */ bool removeProgram(Program *programToRemove); /** * Attempt to remove a program from the queue. The program name is used * as the criteria to decide which object to remove. * @param name The name of the program to be removed from the queue. * @return True on success, false on failure. */ bool removeProgram(const QString &programName); /** * Retrieve the program object associated with the supplied name. * @param name The name of the program. * @return The Program object, a null pointer is returned if the * requested program is not in this queue. */ Program* lookupProgram(const QString &programName) const { return m_programs.value(programName, NULL); } /** * @return A list of program names available through this queue. */ QStringList programNames() const { return m_programs.keys(); } /** * @return A list of the available Program objects. */ QList programs() const { return m_programs.values(); } /** * @return The number of programs belonging to this Queue. */ int numPrograms() const { return m_programs.size(); } /** * @return A string containing a template for the launcher script. For remote * queues, this will be a batch script for the queuing system, for local * queues this will be a shell script (unix) or batch script (windows). * * It should contain the token "$$programExecution$$", which will be replaced * with program-specific launch details. */ virtual QString launchTemplate() const { return m_launchTemplate; } /** * @return The filename for the launcher script. For remote * queues, this will be a batch script for the queuing system, for local * queues this will be a shell script (unix) or batch script (windows). */ QString launchScriptName() const { return m_launchScriptName; } /** * @param moleQueueId MoleQueue id of Job of interest. * @return The number of time the job has failed if it has encountered an * error and is being retried. 0 if the job has not encountered an error, or * has exceeded three retries. */ int jobFailureCount(IdType moleQueueId) const { return m_failureTracker.value(moleQueueId, 0); } /** * @brief replaceKeywords Replace $$keywords$$ in @a launchScript * with queue/job specific values. * @param launchScript Launch script to complete. * @param job Job data to use. */ virtual void replaceKeywords(QString & launchScript, const Job &job, bool addNewline = true); /// For queue creation friend class MoleQueue::QueueManager; signals: /** * Emitted when a new program is added to the Queue. * @param name Name of the program. * @param program Pointer to the newly added Program object. */ void programAdded(const QString &name, MoleQueue::Program *program); /** * Emitted when a program is removed from the queue. * @param name Name of the program * @param program Pointer to the removed Program object. * @warning The @program pointer should not be dereferenced, as this signal * is often associated with program deletion. */ void programRemoved(const QString &name, MoleQueue::Program *program); /** * @brief programRenamed Emitted when a program is renamed. */ void programRenamed(const QString &newName, Program *prog, const QString &oldName); /** * Emitted when the name of the queue is changed. */ void nameChanged(const QString &newName, const QString &oldName); public slots: /** * Writes input files and submits a new job to the queue. * @param job Job to submit. * @return True on success, false on failure. * @sa jobSubmitted */ virtual bool submitJob(MoleQueue::Job job) = 0; /** * @brief killJob Stop the job and remove from the queue. Set the JobState to * Canceled. * @param job Job to kill. */ virtual void killJob(MoleQueue::Job job) = 0; /** * Update the launch script template. * @param script The new launch template. * @sa launchTemplate */ virtual void setLaunchTemplate(const QString &script) { m_launchTemplate = script; } /** * Update the launch script name. * @param scriptName The new launch script name. * @sa launchTemplate * @sa launchScript */ virtual void setLaunchScriptName(const QString &scriptName) { m_launchScriptName = scriptName; } protected slots: /** * Called when the JobManager::jobAboutToBeRemoved signal is emitted to * remove any internal references to the job. Subclasses should reimplement * if they hold any state about owned jobs. */ virtual void jobAboutToBeRemoved(const MoleQueue::Job &job); /** * Update internal data structures when the name of a program changes. */ void programNameChanged(const QString &newName, const QString &oldName); /** * Delete the local working directory of @a Job. */ void cleanLocalDirectory(const MoleQueue::Job &job); protected: /// Write the input files for @a job to the local working directory. bool writeInputFiles(const Job &job); /** * @brief addJobFailure Call this when a job encounters a problem but will be * retried (e.g. a possible networking failure). The failure will be recorded * and the return value will indicate whether to retry the job or not. If this * function returns true, the job has failed less than 3 times and an attempt * should be made to retry. If it returns false, the job has exceeded the * maximum number of retries and should be aborted. The failure count will be * reset and an error will be logged if the maximum retries are exceeded. * @param moleQueueId * @return True if the job should be retried, false otherwise. * @see jobFailureCount * @see clearJobFailures */ bool addJobFailure(IdType moleQueueId); /** * @brief clearJobFailures Remove all recorded job failures for a job. This * does not necessarily mean that the job is successful, but that it is no * longer being retried. * @param moleQueueId MoleQueue id of job * @see addJobFailure * @see jobFailureCount */ void clearJobFailures(IdType moleQueueId) { m_failureTracker.remove(moleQueueId); } QueueManager *m_queueManager; Server *m_server; QString m_name; QString m_launchTemplate; QString m_launchScriptName; QMap m_programs; /// Lookup table for jobs that are using this Queue. Maps JobId to MoleQueueId. QMap m_jobs; /// Keeps track of the number of times a job has failed (MoleQueueId to /// #failures). Once a job fails three times, it will no longer retry. QMap m_failureTracker; private: /// Private helper function bool writeJsonSettingsToFile(const QString &filename, bool exportOnly, bool includePrograms) const; /// Private helper function bool readJsonSettingsFromFile(const QString &filename, bool importOnly, bool includePrograms); }; } // End namespace Q_DECLARE_METATYPE(MoleQueue::Queue*) Q_DECLARE_METATYPE(const MoleQueue::Queue*) #endif // MOLEQUEUE_QUEUE_H molequeue-0.9.0/molequeue/app/queuemanager.cpp000066400000000000000000000133771323436134600215050ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "queuemanager.h" #include "logger.h" #include "queue.h" #include "server.h" #include "molequeueconfig.h" // Concrete queue classes #include "queues/local.h" #include "queues/pbs.h" #include "queues/sge.h" #include "queues/slurm.h" #ifdef MoleQueue_USE_EZHPC_UIT #include "queues/queueuit.h" #endif #include #include #include namespace MoleQueue { QueueManager::QueueManager(Server *parentServer) : QObject(parentServer), m_server(parentServer) { qRegisterMetaType("MoleQueue::Queue*"); qRegisterMetaType("const MoleQueue::Queue*"); } QueueManager::~QueueManager() { QList queueList = m_queues.values(); m_queues.clear(); qDeleteAll(queueList); } void QueueManager::readSettings() { QDir queueDir(queueConfigDirectory()); if (!queueDir.exists()) { Logger::logWarning(tr("Cannot read queue settings: Queue config " "directory does not exist (%1)") .arg(queueDir.absolutePath())); return; } foreach (const QString &queueFileName, queueDir.entryList(QStringList()<<"*.mqq", QDir::Files)) { QString absoluteFileName = queueDir.absoluteFilePath(queueFileName); QString queueName = QFileInfo(queueFileName).baseName(); QString queueType = Queue::queueTypeFromFile(absoluteFileName); Queue *queue = addQueue(queueName, queueType, this); if (queue != NULL) { bool success = queue->readSettings(absoluteFileName); if (!success) { Logger::logError(tr("Cannot load queue '%1' with type '%2' from '%3'. " "Improper configuration file.") .arg(queueName, queueType, absoluteFileName)); removeQueue(queue); queue = NULL; } } else { Logger::logError(tr("Cannot load queue '%1' with type '%2' from '%3'.") .arg(queueName, queueType, absoluteFileName)); } } } void QueueManager::writeSettings() const { foreach (const Queue* queue, queues()) queue->writeSettings(); } QStringList QueueManager::availableQueues() { QStringList result; result << "Local" << "Sun Grid Engine" << "PBS/Torque" << "SLURM"; #ifdef MoleQueue_USE_EZHPC_UIT result << "ezHPC UIT"; #endif return result; } bool QueueManager::queueTypeIsValid(const QString &queueType) { return QueueManager::availableQueues().contains(queueType); } Queue * QueueManager::addQueue(const QString &queueName, const QString &queueType, bool replace) { if (m_queues.contains(queueName)) { if (replace == true) m_queues.take(queueName)->deleteLater(); else return NULL; } Queue * newQueue = NULL; if (queueType== "Local") newQueue = new QueueLocal(this); else if (queueType== "Sun Grid Engine") newQueue = new QueueSge(this); else if (queueType== "PBS/Torque") newQueue = new QueuePbs(this); else if (queueType== "SLURM") newQueue = new QueueSlurm(this); #ifdef MoleQueue_USE_EZHPC_UIT else if (queueType== "ezHPC UIT") newQueue = new QueueUit(this); #endif if (!newQueue) return NULL; newQueue->setName(queueName); connect(newQueue, SIGNAL(nameChanged(QString,QString)), this, SLOT(queueNameChanged(QString,QString))); m_queues.insert(newQueue->name(), newQueue); emit queueAdded(newQueue->name(), newQueue); return newQueue; } bool QueueManager::removeQueue(const Queue *queue) { return removeQueue(queue->name()); } bool QueueManager::removeQueue(const QString &name) { if (!m_queues.contains(name)) return false; Queue *queue = m_queues.take(name); emit queueRemoved(name, queue); QString fileName = queue->stateFileName(); queue->deleteLater(); // Remove state file: if (!fileName.isEmpty()) QFile::remove(fileName); return true; } QueueListType QueueManager::toQueueList() const { QueueListType queueList; foreach(const Queue *queue, m_queues) queueList.insert(queue->name(), queue->programNames()); return queueList; } void QueueManager::updateRemoteQueues() const { foreach (Queue *queue, m_queues) { if (QueueRemote *remote = qobject_cast(queue)) { remote->requestQueueUpdate(); } } } void QueueManager::queueNameChanged(const QString &newName, const QString &oldName) { if (Queue *queue = m_queues.value(oldName, NULL)) { if (queue->name() == newName) { // Rewrite the configuration file: QString fileName = queue->stateFileName(); if (!fileName.isEmpty()) QFile::remove(fileName); m_queues.remove(oldName); m_queues.insert(newName, queue); queue->writeSettings(); emit queueRenamed(newName, queue, oldName); } } } QString QueueManager::queueConfigDirectory() const { QString result; if (m_server) { result = m_server->workingDirectoryBase(); } else { QSettings settings; result = settings.value("workingDirectoryBase").toString(); } if (result.isEmpty()) { Logger::logError(tr("Cannot determine queue config directory.")); return result; } return result + "/config/queues"; } } // end MoleQueue namespace molequeue-0.9.0/molequeue/app/queuemanager.h000066400000000000000000000113001323436134600211320ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_QUEUEMANAGER_H #define MOLEQUEUE_QUEUEMANAGER_H #include #include "molequeueglobal.h" #include namespace MoleQueue { class Queue; class Server; /** * @class QueueManager queuemanager.h * @brief Manage a collection of Queue instances. * @author David C. Lonie */ class QueueManager : public QObject { Q_OBJECT public: explicit QueueManager(Server *parentServer = 0); ~QueueManager(); void readSettings(); void writeSettings() const; /// @return The parent Server Server *server() {return m_server;} /// @return The parent Server const Server *server() const {return m_server;} /** * @param name String containing the name of the queue of interest. * @return The requested Queue, or NULL if none exist with that name. */ Queue * lookupQueue(const QString &name) const { return m_queues.value(name, NULL); } /** * @return A list of available queues types (e.g. PBS/Torque, SGE, etc.) */ static QStringList availableQueues(); /** * @param queueType Type of Queue (SGE, PBS/Torque, Local, etc) * @return True if the queue can be instantiated, false otherwise. */ static bool queueTypeIsValid(const QString &queueType); /** * Add a new Queue to the QueueManager. The new queueName must be unique name. * The QueueManager maintains ownership of the Queue. * @param queueName Unique, user-visible name of the new Queue object. * @param queueType The type of the new Queue object, e.g. PBS/Torque, SGE, etc/ * @param replace Defaults to false; if true, replace any existing queues with * the same name. The old queue with the same name will be deleted. * @return A pointer to the new queue if successful, NULL otherwise. * @sa queueTypeIsKnown * @sa availableQueueTypes */ virtual Queue * addQueue(const QString &queueName, const QString &queueType, bool replace = false); /** * Remove and delete a queue from the collection. * @param queue Queue to remove. * @return True if queue exists, false otherwise. */ bool removeQueue(const Queue *queue); /** * Remove and delete a queue from the collection. * @param queueName Name of queue to remove. * @return True if queue exists, false otherwise. */ bool removeQueue(const QString &name); /** * @return A list of all Queue objects in the QueueManager. */ QList queues() const { return m_queues.values(); } /** * @return Names of all Queue objects known to the QueueManager. */ QStringList queueNames() const { return m_queues.keys(); } /** * @return The number of Queue objects known to the QueueManager. */ int numQueues() const { return m_queues.size(); } /** * @return A QueueListType container describing the queues and their * associated programs. */ QueueListType toQueueList() const; /// @return The directory path where queue configuration files are stored. QString queueConfigDirectory() const; public slots: /** * @brief updateRemoteQueues Request that all remote queues update the status * of their jobs. */ void updateRemoteQueues() const; signals: /** * Emitted when a new Queue is added to the QueueManager * @param name Name of the new Queue * @param queue Pointer to the Queue. */ void queueAdded(const QString &name, MoleQueue::Queue *queue); /** * Emitted when a Queue is removed from the QueueManager * @param name Name of the new Queue * @param queue Pointer to the Queue. */ void queueRemoved(const QString &name, MoleQueue::Queue *queue); /** * @brief queueRenamed Emitted when a queue is renamed. */ void queueRenamed(const QString &newName, MoleQueue::Queue *queue, const QString &oldName); protected: QMap m_queues; Server *m_server; private slots: /// Update the internal data structures when a queue changes names void queueNameChanged(const QString &newName, const QString &oldName); }; } // end MoleQueue namespace #endif // MOLEQUEUE_QUEUEMANAGER_H molequeue-0.9.0/molequeue/app/queuemanagerdialog.cpp000066400000000000000000000163411323436134600226570ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "queuemanagerdialog.h" #include "ui_queuemanagerdialog.h" #include #include #include #include #include #include "queue.h" #include "logger.h" #include "mainwindow.h" #include "addqueuedialog.h" #include "importqueuedialog.h" #include "queuemanager.h" #include "queuemanageritemmodel.h" #include "queuesettingsdialog.h" namespace MoleQueue { QueueManagerDialog::QueueManagerDialog(QueueManager *queueManager, QWidget *parentObject) : QDialog(parentObject), ui(new Ui::QueueManagerDialog), m_queueManager(queueManager), m_queueManagerItemModel(new QueueManagerItemModel (m_queueManager, this)) { ui->setupUi(this); ui->queueTable->setModel(m_queueManagerItemModel); ui->queueTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); connect(ui->queueTable, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(doubleClicked(QModelIndex))); connect(ui->addQueueButton, SIGNAL(clicked()), this, SLOT(addQueue())); connect(ui->removeQueueButton, SIGNAL(clicked()), this, SLOT(removeQueue())); connect(ui->configureQueueButton, SIGNAL(clicked()), this, SLOT(configureQueue())); connect(ui->importQueueButton, SIGNAL(clicked()), this, SLOT(importQueue())); connect(ui->exportQueueButton, SIGNAL(clicked()), this, SLOT(exportQueue())); connect(ui->queueTable->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(enableQueueButtons(QItemSelection))); } QueueManagerDialog::~QueueManagerDialog() { delete ui; } void QueueManagerDialog::addQueue() { AddQueueDialog dialog(m_queueManager, this); dialog.exec(); } void QueueManagerDialog::removeQueue() { QList toRemove = getSelectedQueues(); foreach (Queue* queue, toRemove) { m_queueManager->removeQueue(queue); queue->deleteLater(); } // reset selection and disable queue buttons ui->queueTable->selectionModel()->reset(); setEnabledQueueButtons(false); } void QueueManagerDialog::configureQueue() { QList sel = getSelectedQueues(); if (!sel.isEmpty()) showSettingsDialog(sel.first()); } void QueueManagerDialog::importQueue() { ImportQueueDialog dialog(m_queueManager, this); dialog.exec(); } void QueueManagerDialog::exportQueue() { // Get selected Queue QList selectedQueues = getSelectedQueues(); // Ensure that only one queue is selected at a time if (selectedQueues.size() < 1) return; if (selectedQueues.size() != 1) { QMessageBox::information(this, tr("Queue Export"), tr("Please select only one queue to export at a " "time."), QMessageBox::Ok); return; } Queue *queue = selectedQueues.first(); // Get initial dir: QSettings settings; QString initialDir = settings.value("export/queue/lastExportFile", QDir::homePath()).toString(); initialDir = QFileInfo(initialDir).dir().absolutePath() + QString("/%1.mqq").arg(queue->name()); // Get filename for export QString exportFileName = QFileDialog::getSaveFileName(this, tr("Select export filename"), initialDir, tr("MoleQueue Queue Export Format (*.mqq);;" "All files (*)")); // User cancel: if (exportFileName.isNull()) return; // Set location for next time settings.setValue("export/queue/lastExportFile", exportFileName); // Prompt whether to export all programs or just the queue details QMessageBox::StandardButton exportProgramsButton = QMessageBox::question(this, tr("Export programs?"), tr("Would you like to export all program " "configurations along with the queue?\n\n" "Programs: %1") .arg(queue->programNames().join(", ")), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); bool exportPrograms = (exportProgramsButton == QMessageBox::Yes); // Populate file if (!queue->exportSettings(exportFileName, exportPrograms)) { QMessageBox::critical(this, tr("Queue Export"), tr("Could not export queue. Check the log for " "details."), QMessageBox::Ok); } } void QueueManagerDialog::doubleClicked(const QModelIndex &index) { if (index.row() <= m_queueManager->numQueues()) showSettingsDialog(m_queueManager->queues().at(index.row())); } void QueueManagerDialog::showSettingsDialog(Queue *queue) { QueueSettingsDialog *dialog = NULL; // Check if there is already an open dialog for this queue dialog = m_queueSettingsDialogs.value(queue, NULL); // If not, create one if (!dialog) { dialog = new QueueSettingsDialog(queue, this); m_queueSettingsDialogs.insert(queue, dialog); connect(dialog, SIGNAL(finished(int)), this, SLOT(removeSettingsDialog())); } // Show and raise the dialog dialog->show(); dialog->raise(); } void QueueManagerDialog::removeSettingsDialog() { QueueSettingsDialog *dialog = qobject_cast(sender()); if (!dialog) { Logger::logDebugMessage(tr("Internal error in %1: Sender is not a " "QueueSettingsDialog (sender() = %2") .arg(Q_FUNC_INFO) .arg(sender() ? sender()->metaObject()->className() : "NULL")); return; } m_queueSettingsDialogs.remove(dialog->currentQueue()); dialog->deleteLater(); } QList QueueManagerDialog::getSelectedRows() { QItemSelection sel (ui->queueTable->selectionModel()->selection()); QList rows; foreach (const QModelIndex &ind, sel.indexes()) { if (!rows.contains(ind.row())) rows << ind.row(); } qSort(rows); return rows; } QList QueueManagerDialog::getSelectedQueues() { QList allQueues = m_queueManager->queues(); QList selectedQueues; foreach (int i, getSelectedRows()) selectedQueues << allQueues.at(i); return selectedQueues; } void QueueManagerDialog::setEnabledQueueButtons(bool enabled) { ui->removeQueueButton->setEnabled(enabled); ui->configureQueueButton->setEnabled(enabled); ui->exportQueueButton->setEnabled(enabled); } void QueueManagerDialog::enableQueueButtons(const QItemSelection &selected) { setEnabledQueueButtons(!selected.isEmpty()); } } // end MoleQueue namespace molequeue-0.9.0/molequeue/app/queuemanagerdialog.h000066400000000000000000000034521323436134600223230ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef QUEUEMANAGERDIALOG_H #define QUEUEMANAGERDIALOG_H #include #include namespace Ui { class QueueManagerDialog; } namespace MoleQueue { class Queue; class QueueManager; class QueueManagerItemModel; class QueueSettingsDialog; /// @brief Dialog for managing supported queues. class QueueManagerDialog : public QDialog { Q_OBJECT public: explicit QueueManagerDialog(QueueManager *manager, QWidget *parentObject = 0); ~QueueManagerDialog(); protected slots: void addQueue(); void removeQueue(); void configureQueue(); void importQueue(); void exportQueue(); void doubleClicked(const QModelIndex &); void showSettingsDialog(MoleQueue::Queue *queue); void removeSettingsDialog(); void enableQueueButtons(const QItemSelection &selected); protected: /// Row indices, ascending order QList getSelectedRows(); QList getSelectedQueues(); void setEnabledQueueButtons(bool enabled); Ui::QueueManagerDialog *ui; QueueManager *m_queueManager; QueueManagerItemModel *m_queueManagerItemModel; QMap m_queueSettingsDialogs; }; } // end MoleQueue namespace #endif // QUEUEMANAGERDIALOG_H molequeue-0.9.0/molequeue/app/queuemanageritemmodel.cpp000066400000000000000000000070631323436134600234000ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "queuemanageritemmodel.h" #include "queue.h" #include "queuemanager.h" namespace MoleQueue { QueueManagerItemModel::QueueManagerItemModel(QueueManager *queueManager, QObject *parentObject) : QAbstractItemModel(parentObject), m_queueManager(queueManager) { connect(m_queueManager, SIGNAL(queueAdded(QString,MoleQueue::Queue*)), this, SLOT(callReset())); connect(m_queueManager, SIGNAL(queueRemoved(QString,MoleQueue::Queue*)), this, SLOT(callReset())); connect(m_queueManager, SIGNAL(queueRenamed(QString,MoleQueue::Queue*,QString)), this, SLOT(callReset())); } int QueueManagerItemModel::rowCount(const QModelIndex &modelIndex) const { if (!modelIndex.isValid()) return m_queueManager->numQueues(); else return 0; } int QueueManagerItemModel::columnCount(const QModelIndex &) const { return COLUMN_COUNT; } QVariant QueueManagerItemModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { switch (static_cast(section)) { case QUEUE_NAME: return QVariant("Queue"); case QUEUE_TYPE: return QVariant("Type"); case NUM_PROGRAMS: return QVariant("# Programs"); case PROGRAM_NAMES: return QVariant("Program names"); case COLUMN_COUNT: default: return QVariant(); } } return QVariant(); } QVariant QueueManagerItemModel::data(const QModelIndex &modelIndex, int role) const { if (!modelIndex.isValid() || modelIndex.column() >= COLUMN_COUNT || modelIndex.row() >= m_queueManager->numQueues()) return QVariant(); const Queue *queue = m_queueManager->queues().at(modelIndex.row()); if (queue) { if (role == Qt::DisplayRole) { switch (static_cast(modelIndex.column())) { case QUEUE_NAME: return queue->name(); case QUEUE_TYPE: return queue->typeName(); case NUM_PROGRAMS: return queue->numPrograms(); case PROGRAM_NAMES: if (queue->numPrograms() != 0) return queue->programNames().join(", "); else return QVariant("None"); case COLUMN_COUNT: default: return QVariant(); } } } return QVariant(); } Qt::ItemFlags QueueManagerItemModel::flags(const QModelIndex &modelIndex) const { if (modelIndex.column() == 0) return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsUserCheckable; else return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } QModelIndex QueueManagerItemModel::index(int row, int column, const QModelIndex &) const { if (row >= 0 && row < m_queueManager->numQueues()) return createIndex(row, column); else return QModelIndex(); } } // end namespace MoleQueue molequeue-0.9.0/molequeue/app/queuemanageritemmodel.h000066400000000000000000000041151323436134600230400ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_QUEUEMANAGERITEMMODEL_H #define MOLEQUEUE_QUEUEMANAGERITEMMODEL_H #include namespace MoleQueue { class QueueManager; /// @brief Item model for interacting with queues in the QueueManagerDialog. class QueueManagerItemModel : public QAbstractItemModel { Q_OBJECT enum ColumnNames { QUEUE_NAME, QUEUE_TYPE, NUM_PROGRAMS, PROGRAM_NAMES, COLUMN_COUNT // Use to get the total number of columns }; public: explicit QueueManagerItemModel(QueueManager *queueManager, QObject *parentObject = 0); QModelIndex parent(const QModelIndex &) const {return QModelIndex();} int rowCount(const QModelIndex & modelIndex = QModelIndex()) const; int columnCount(const QModelIndex & modelIndex = QModelIndex()) const; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; QVariant data(const QModelIndex & modelIndex, int role = Qt::DisplayRole) const; Qt::ItemFlags flags(const QModelIndex & modelIndex) const; QModelIndex index(int row, int column, const QModelIndex & modelIndex = QModelIndex()) const; protected: QueueManager *m_queueManager; private slots: /// FIXME: This needs to be fixed to call begin/end reset at the right times. void callReset() { beginResetModel(); endResetModel(); } }; } // end namespace MoleQueue #endif // QUEUEMANAGERITEMMODEL_H molequeue-0.9.0/molequeue/app/queueprogramitemmodel.cpp000066400000000000000000000074361323436134600234410ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "queueprogramitemmodel.h" #include "program.h" #include "queue.h" namespace MoleQueue { QueueProgramItemModel::QueueProgramItemModel(Queue *queue, QObject *parentObject) : QAbstractItemModel(parentObject), m_queue(queue) { connect(m_queue, SIGNAL(programRenamed(QString,Program*,QString)), this, SLOT(callReset())); } bool QueueProgramItemModel::addProgram(Program *program) { if (!m_queue) return false; QString progName(program->name()); QStringList progNames(m_queue->programNames()); if (progName.isEmpty() || progNames.contains(progName)) { return false; } // Determine the model row that will be created: int idx; for (idx = 0; idx < progNames.size() && progName < progNames[idx]; ++idx) {} beginInsertRows(QModelIndex(), idx, idx); m_queue->addProgram(program, false); endInsertRows(); return true; } bool QueueProgramItemModel::removeProgram(Program *program) { if (!m_queue) return false; int idx = m_queue->programs().indexOf(program); if (idx < 0) return false; beginRemoveRows(QModelIndex(), idx, idx); m_queue->removeProgram(program); endRemoveRows(); return true; } QModelIndex QueueProgramItemModel::parent(const QModelIndex &) const { return QModelIndex(); } int QueueProgramItemModel::rowCount(const QModelIndex &modelIndex) const { if (!modelIndex.isValid()) return m_queue->numPrograms(); else return 0; } int QueueProgramItemModel::columnCount(const QModelIndex &/*modelIndex*/) const { return COLUMN_COUNT; } QVariant QueueProgramItemModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { switch (static_cast(section)) { case PROGRAM_NAME: return QVariant("Program"); case COLUMN_COUNT: default: return QVariant(); } } return QVariant(); } QVariant QueueProgramItemModel::data(const QModelIndex &modelIndex, int role) const { if (!modelIndex.isValid() || modelIndex.column() >= COLUMN_COUNT || modelIndex.row() >= m_queue->numPrograms()) return QVariant(); const Program *program = m_queue->programs().at(modelIndex.row()); if (program) { if (role == Qt::DisplayRole) { switch (static_cast(modelIndex.column())) { case PROGRAM_NAME: return program->name(); case COLUMN_COUNT: default: return QVariant(); } } } return QVariant(); } Qt::ItemFlags QueueProgramItemModel::flags(const QModelIndex &) const { return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } QModelIndex QueueProgramItemModel::index(int row, int column, const QModelIndex &) const { if (row >= 0 && row < m_queue->numPrograms()) return createIndex(row, column); else return QModelIndex(); } void QueueProgramItemModel::callReset() { /// FIXME: replace this with actual calls to begin/end reset at the right times. beginResetModel(); endResetModel(); } } // End of namespace molequeue-0.9.0/molequeue/app/queueprogramitemmodel.h000066400000000000000000000035421323436134600231000ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef QUEUEITEMMODEL_H #define QUEUEITEMMODEL_H #include namespace MoleQueue { class Queue; class Program; /// @brief Item model for interacting with a Queue's Program instances. class QueueProgramItemModel : public QAbstractItemModel { Q_OBJECT enum ColumnNames { PROGRAM_NAME = 0, COLUMN_COUNT // Use to get total number of columns }; public: explicit QueueProgramItemModel(Queue *queue, QObject *parentObject = 0); bool addProgram(Program *program); bool removeProgram(Program *program); QModelIndex parent(const QModelIndex & modelIndex) const; int rowCount(const QModelIndex & modelIndex = QModelIndex()) const; int columnCount(const QModelIndex & modelIndex = QModelIndex()) const; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; QVariant data(const QModelIndex & modelIndex, int role = Qt::DisplayRole) const; Qt::ItemFlags flags(const QModelIndex & modelIndex) const; QModelIndex index(int row, int column, const QModelIndex & modelIndex = QModelIndex()) const; protected slots: void callReset(); protected: Queue *m_queue; }; } // End namespace #endif molequeue-0.9.0/molequeue/app/queues/000077500000000000000000000000001323436134600176165ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/queues/local.cpp000066400000000000000000000334711323436134600214240ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "local.h" #include "../filesystemtools.h" #include "../job.h" #include "../jobmanager.h" #include "../localqueuewidget.h" #include "../logentry.h" #include "../logger.h" #include "../program.h" #include "../queue.h" #include "../queuemanager.h" #include "../server.h" #include #include #include #include #include #include #include #include #include // For ideal thread count #include #include #include #ifdef _WIN32 #include // For _PROCESS_INFORMATION (PID parsing) #endif namespace MoleQueue { QueueLocal::QueueLocal(QueueManager *parentManager) : Queue("Local", parentManager), m_checkJobLimitTimerId(-1), m_cores(-1) { #ifdef _WIN32 m_launchTemplate = "@echo off\n\n$$programExecution$$\n"; m_launchScriptName = "MoleQueueLauncher.bat"; #else // WIN32 m_launchTemplate = "#!/bin/bash\n\n$$programExecution$$\n"; m_launchScriptName = "MoleQueueLauncher.sh"; #endif // WIN32 // Check if new jobs need starting every 100 ms m_checkJobLimitTimerId = startTimer(100); } QueueLocal::~QueueLocal() { } bool QueueLocal::writeJsonSettings(QJsonObject &json, bool exportOnly, bool includePrograms) const { if (!Queue::writeJsonSettings(json, exportOnly, includePrograms)) return false; json.insert("cores", static_cast(m_cores)); if (!exportOnly) { QJsonArray jobsToResumeArray; foreach (IdType jobId, m_runningJobs.keys()) jobsToResumeArray.append(idTypeToJson(jobId)); foreach (IdType jobId, m_pendingJobQueue) jobsToResumeArray.append(idTypeToJson(jobId)); json.insert("jobsToResume", jobsToResumeArray); } return true; } bool QueueLocal::readJsonSettings(const QJsonObject &json, bool importOnly, bool includePrograms) { // Validate JSON if (!json.value("cores").isDouble()) { Logger::logError(tr("Error reading queue settings: Invalid format:\n%1") .arg(QString(QJsonDocument(json).toJson()))); return false; } QList jobsToResume; if (!importOnly && json.contains("jobsToResume")) { if (!json.value("jobsToResume").isArray()) { Logger::logError(tr("Error reading queue settings: Invalid format:\n%1") .arg(QString(QJsonDocument(json).toJson()))); return false; } QJsonArray jobsToResumeArray = json.value("jobsToResume").toArray(); foreach (QJsonValue val, jobsToResumeArray) { if (!val.isDouble()) { Logger::logError(tr("Error reading queue settings: Invalid format:\n%1") .arg(QString(QJsonDocument(json).toJson()))); return false; } jobsToResume.append(toIdType(val)); } } if (!Queue::readJsonSettings(json, importOnly, includePrograms)) return false; // Everything is validated -- go ahead and update object. m_cores = static_cast(json.value("cores").toDouble() + 0.5); m_pendingJobQueue = jobsToResume; return true; } AbstractQueueSettingsWidget *QueueLocal::settingsWidget() { LocalQueueWidget *widget = new LocalQueueWidget(this); return widget; } bool QueueLocal::submitJob(Job job) { if (job.isValid()) { Job(job).setJobState(MoleQueue::Accepted); return prepareJobForSubmission(job);; } Logger::logError(tr("Refusing to submit job to Queue '%1': Job object is " "invalid.").arg(m_name), job.moleQueueId()); return false; } void QueueLocal::killJob(Job job) { if (!job.isValid()) return; int pendingIndex = m_pendingJobQueue.indexOf(job.moleQueueId()); if (pendingIndex >= 0) { m_pendingJobQueue.removeAt(pendingIndex); job.setJobState(MoleQueue::Canceled); return; } QProcess *process = m_runningJobs.take(job.moleQueueId()); if (process != NULL) { m_jobs.remove(job.queueId()); process->disconnect(this); process->terminate(); process->deleteLater(); job.setJobState(MoleQueue::Canceled); return; } job.setJobState(MoleQueue::Canceled); } bool QueueLocal::prepareJobForSubmission(Job &job) { if (!writeInputFiles(job)) { Logger::logError(tr("Error while writing input files."), job.moleQueueId()); job.setJobState(Error); return false; } if (!addJobToQueue(job)) return false; return true; } void QueueLocal::processStarted() { QProcess *process = qobject_cast(sender()); if (!process) return; IdType moleQueueId = m_runningJobs.key(process, 0); if (moleQueueId == 0) return; IdType queueId; #ifdef _WIN32 queueId = static_cast(process->pid()->dwProcessId); #else // WIN32 queueId = static_cast(process->pid()); #endif // WIN32 // Get pointer to jobmanager to lookup job if (!m_server) { Logger::logError(tr("Queue '%1' cannot locate Server instance!") .arg(m_name), moleQueueId); return; } Job job = m_server->jobManager()->lookupJobByMoleQueueId(moleQueueId); if (!job.isValid()) { Logger::logError(tr("Queue '%1' Cannot update invalid Job reference!") .arg(m_name), moleQueueId); return; } job.setQueueId(queueId); job.setJobState(MoleQueue::RunningLocal); } void QueueLocal::processFinished(int exitCode, QProcess::ExitStatus exitStatus) { Q_UNUSED(exitCode); Q_UNUSED(exitStatus); QProcess *process = qobject_cast(sender()); if (!process) return; IdType moleQueueId = m_runningJobs.key(process, 0); if (moleQueueId == 0) return; // Remove and delete QProcess from queue m_runningJobs.take(moleQueueId)->deleteLater(); // Get pointer to jobmanager to lookup job if (!m_server) { Logger::logError(tr("Queue '%1' cannot locate Server instance!") .arg(m_name), moleQueueId); return; } Job job = m_server->jobManager()->lookupJobByMoleQueueId(moleQueueId); if (!job.isValid()) { Logger::logDebugMessage(tr("Queue '%1' Cannot update invalid Job " "reference!").arg(m_name), moleQueueId); return; } if (!job.outputDirectory().isEmpty() && job.outputDirectory() != job.localWorkingDirectory()) { // copy function logs errors if needed if (!FileSystemTools::recursiveCopyDirectory(job.localWorkingDirectory(), job.outputDirectory())) { Logger::logError(tr("Cannot copy '%1' -> '%2'.") .arg(job.localWorkingDirectory(), job.outputDirectory()), job.moleQueueId()); job.setJobState(MoleQueue::Error); return; } } if (job.cleanLocalWorkingDirectory()) cleanLocalDirectory(job); job.setJobState(MoleQueue::Finished); } int QueueLocal::maxNumberOfCores() const { if (m_cores > 0) return m_cores; else return QThread::idealThreadCount(); } bool QueueLocal::addJobToQueue(const Job &job) { m_pendingJobQueue.append(job.moleQueueId()); Job(job).setJobState(MoleQueue::QueuedLocal); return true; } void QueueLocal::connectProcess(QProcess *proc) { connect(proc, SIGNAL(started()), this, SLOT(processStarted())); connect(proc, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(processFinished(int,QProcess::ExitStatus))); connect(proc, SIGNAL(error(QProcess::ProcessError)), this, SLOT(processError(QProcess::ProcessError))); } void QueueLocal::checkJobQueue() { if (m_pendingJobQueue.isEmpty()) return; int coresInUse = 0; foreach(IdType moleQueueId, m_runningJobs.keys()) { const Job job = m_server->jobManager()->lookupJobByMoleQueueId(moleQueueId); if (job.isValid()) coresInUse += job.numberOfCores(); } int totalCores = maxNumberOfCores(); int coresAvailable = totalCores - coresInUse; // Keep submitting jobs (FIFO) until we hit one we can't afford to start. while (!m_pendingJobQueue.isEmpty() && coresAvailable > 0) { IdType nextMQId = m_pendingJobQueue.first(); Job nextJob = m_server->jobManager()->lookupJobByMoleQueueId(nextMQId); if (!nextJob.isValid()) { m_pendingJobQueue.removeFirst(); continue; } else if (nextJob.numberOfCores() <= coresAvailable) { m_pendingJobQueue.removeFirst(); if (startJob(nextJob.moleQueueId())) coresAvailable -= nextJob.numberOfCores(); continue; } // Cannot start next job yet! break; } } bool QueueLocal::startJob(IdType moleQueueId) { // Get pointers to job, server, etc if (!m_server) { Logger::logError(tr("Queue '%1' cannot locate Server instance!") .arg(m_name), moleQueueId); return false; } const Job job = m_server->jobManager()->lookupJobByMoleQueueId(moleQueueId); if (!job.isValid()) { Logger::logError(tr("Queue '%1' cannot locate Job with MoleQueue id %2.") .arg(m_name).arg(idTypeToString(moleQueueId)), moleQueueId); return false; } const Program *program = lookupProgram(job.program()); if (!program) { Logger::logError(tr("Queue '%1' cannot locate Program '%2'.") .arg(m_name).arg(job.program()), moleQueueId); return false; } FileSpecification inputFileSpec(job.inputFile()); // Create and setup process QProcess *proc = new QProcess (this); QDir dir (job.localWorkingDirectory()); proc->setWorkingDirectory(dir.absolutePath()); QStringList arguments; if (!program->arguments().isEmpty()) arguments << program->arguments(); QString command; // Set default command. May be overwritten later. command = program->executable(); switch (program->launchSyntax()) { case Program::CUSTOM: #ifdef _WIN32 command = "cmd.exe /c " + launchScriptName(); #else // WIN32 command = "./" + launchScriptName(); #endif // WIN32 break; case Program::PLAIN: break; case Program::INPUT_ARG: arguments << inputFileSpec.filename(); break; case Program::INPUT_ARG_NO_EXT: arguments << inputFileSpec.fileBaseName(); break; case Program::REDIRECT: { proc->setStandardInputFile(dir.absoluteFilePath(inputFileSpec.filename())); QString outputFilename(program->outputFilename()); replaceKeywords(outputFilename, job, false); proc->setStandardOutputFile(dir.absoluteFilePath(outputFilename)); } break; case Program::INPUT_ARG_OUTPUT_REDIRECT: { arguments << inputFileSpec.filename(); QString outputFilename(program->outputFilename()); replaceKeywords(outputFilename, job, false); proc->setStandardOutputFile(dir.absoluteFilePath(outputFilename)); } break; case Program::SYNTAX_COUNT: default: Logger::logError(tr("Unknown launcher syntax for program %1: %2.") .arg(job.program()).arg(program->launchSyntax()), moleQueueId); return false; } connectProcess(proc); // Handle any keywords in the arguments QString args = arguments.join(" "); replaceKeywords(args, job, false); Logger::logNotification(tr("Executing '%1 %2' in %3", "command, args, dir") .arg(command).arg(args) .arg(proc->workingDirectory()), job.moleQueueId()); m_runningJobs.insert(job.moleQueueId(), proc); proc->start(command + " " + args); return true; } void QueueLocal::timerEvent(QTimerEvent *theEvent) { if (theEvent->timerId() == m_checkJobLimitTimerId) { checkJobQueue(); theEvent->accept(); return; } QObject::timerEvent(theEvent); } void QueueLocal::processError(QProcess::ProcessError error) { QProcess *process = qobject_cast(sender()); if (!process) return; IdType moleQueueId = m_runningJobs.key(process, 0); if (moleQueueId == 0) return; // Remove and delete QProcess from queue m_runningJobs.take(moleQueueId)->deleteLater(); if (!m_server) { Logger::logError(tr("Queue '%1' cannot locate Server instance!") .arg(m_name), moleQueueId); return; } Job job = m_server->jobManager()->lookupJobByMoleQueueId(moleQueueId); if (!job.isValid()) { Logger::logDebugMessage(tr("Queue '%1' Cannot update invalid Job " "reference!").arg(m_name), moleQueueId); return; } QString errorString = QueueLocal::processErrorToString(error); Logger::logError(tr("Execution of \'%1\' failed with process \'%2\': %3") .arg(job.program()).arg(errorString) .arg(process->errorString()), moleQueueId); job.setJobState(MoleQueue::Error); } /** * Convert a ProcessError value to a string. * * @param error ProcessError * @return C string */ QString QueueLocal::processErrorToString(QProcess::ProcessError error) { switch(error) { case QProcess::FailedToStart: return tr("Failed to start"); case QProcess::Crashed: return tr("Crashed"); case QProcess::Timedout: return tr("Timed out"); case QProcess::WriteError: return tr("Write error"); case QProcess::ReadError: return tr("Read error"); case QProcess::UnknownError: return tr("Unknown error"); } Logger::logError(tr("Unrecognized Process Error: %1").arg(error)); return tr("Unrecognized process error"); } } // End namespace molequeue-0.9.0/molequeue/app/queues/local.h000066400000000000000000000061201323436134600210600ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef QUEUELOCAL_H #define QUEUELOCAL_H #include "../queue.h" #include class QThread; namespace MoleQueue { class Job; class QueueManager; /// @brief Queue for running jobs locally. class QueueLocal : public Queue { Q_OBJECT public: explicit QueueLocal(QueueManager *parentManager = 0); ~QueueLocal(); QString typeName() const { return "Local"; } bool writeJsonSettings(QJsonObject &json, bool exportOnly, bool includePrograms) const; bool readJsonSettings(const QJsonObject &json, bool importOnly, bool includePrograms); /** * Returns a widget that can be used to configure the settings for the * queue. */ AbstractQueueSettingsWidget* settingsWidget(); /// The number of cores available. int maxNumberOfCores() const; /// The number of cores available. void setMaxNumberOfCores(int cores) { m_cores = cores; } public slots: bool submitJob(MoleQueue::Job job); void killJob(MoleQueue::Job job); protected slots: /** * Write the input files for the job and add to the queue * @param job The Job. * @return True on success, false otherwise. */ bool prepareJobForSubmission(Job &job); /** * Called when a process starts. */ void processStarted(); /** * Called when a process exits. * @param exitCode Exit code of process * @param exitStatus Exit status of process */ void processFinished(int exitCode, QProcess::ExitStatus exitStatus); /** * Called when a error occurs with a process. * @param error the specific error that occurred */ void processError(QProcess::ProcessError error); protected: /// Insert the job into the queue. bool addJobToQueue(const Job &job); /// Connect @a proc to handlers prior to submitting job void connectProcess(QProcess *proc); /// Submit any queued jobs that can be started void checkJobQueue(); /// Submit the job with MoleQueue id @a moleQueueId. bool startJob(IdType moleQueueId); /// Reimplemented to monitor queue events. void timerEvent(QTimerEvent *theEvent); /// Internal timer id. int m_checkJobLimitTimerId; /// FIFO queue of MoleQueue ids. QList m_pendingJobQueue; /// List of running processes. MoleQueue Id to QProcess* QMap m_runningJobs; /// The number of cores available. int m_cores; private: static QString processErrorToString(QProcess::ProcessError error); }; } // End namespace #endif // QUEUELOCAL_H molequeue-0.9.0/molequeue/app/queues/pbs.cpp000066400000000000000000000065751323436134600211230ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "pbs.h" #include "logger.h" #include namespace MoleQueue { QueuePbs::QueuePbs(QueueManager *parentManager) : QueueRemoteSsh("Remote (PBS)", parentManager) { m_submissionCommand = "qsub"; m_killCommand = "qdel"; m_requestQueueCommand = "qstat"; m_launchScriptName = "job.pbs"; m_launchTemplate = "#!/bin/sh\n" "#\n" "# Sample job script provided by MoleQueue.\n" "#\n" "#These commands set up the Grid Environment for your job:\n" "#PBS -N MoleQueueJob-$$moleQueueId$$\n" "#PBS -l procs=$$numberOfCores$$\n" "#PBS -l walltime=$$maxWallTime$$\n" "\n" "cd $PBS_O_WORKDIR\n" "$$programExecution$$\n"; // qstat will return an exit code of 153 if a job has completed. m_allowedQueueRequestExitCodes.append(153); // unless it's an ezHPC fork. Then it will return 35. m_allowedQueueRequestExitCodes.append(35); } QueuePbs::~QueuePbs() { } bool QueuePbs::parseQueueId(const QString &submissionOutput, IdType *queueId) { // Assuming submissionOutput is: // . QRegExp parser ("^(\\d+)"); int ind = parser.indexIn(submissionOutput); if (ind >= 0) { bool ok; *queueId = static_cast(parser.cap(1).toInt(&ok)); return ok; } return false; } bool QueuePbs::parseQueueLine(const QString &queueListOutput, IdType *queueId, JobState *state) { // Expecting qstat output is: // Job id Name User Time Use S Queue // ---------------- ---------------- ---------------- -------- - ----- // 4807 scatter user01 12:56:34 R batch QRegExp parser ("^\\s*(\\d+)\\S*" // job-ID "\\s+\\S+" // name "\\s+\\S+" // user "\\s+\\S+" // time "\\s+(\\w+)"); // state QString stateStr; int ind = parser.indexIn(queueListOutput); if (ind >= 0) { bool ok; *queueId = static_cast(parser.cap(1).toInt(&ok)); if (!ok) return false; stateStr = parser.cap(2).toLower(); if (stateStr == "r" || stateStr == "e" || stateStr == "c") { *state = MoleQueue::RunningRemote; return true; } else if (stateStr == "q" || stateStr == "h" || stateStr == "t" || stateStr == "w" || stateStr == "s") { *state = MoleQueue::QueuedRemote; return true; } else { Logger::logWarning(tr("Unrecognized queue state '%1' in %2 queue '%3'. " "Queue line:\n%4") .arg(stateStr).arg(typeName()).arg(name()) .arg(queueListOutput)); return false; } } return false; } } // End namespace molequeue-0.9.0/molequeue/app/queues/pbs.h000066400000000000000000000024231323436134600205540ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef QUEUEPBS_H #define QUEUEPBS_H #include "remotessh.h" class QueuePbsTest; namespace MoleQueue { /// @brief QueueRemote subclass for interacting with a PBS/Torque queue. class QueuePbs : public QueueRemoteSsh { Q_OBJECT public: explicit QueuePbs(QueueManager *parentManager = 0); ~QueuePbs(); QString typeName() const { return "PBS/Torque"; } friend class ::QueuePbsTest; protected: virtual bool parseQueueId(const QString &submissionOutput, IdType *queueId); virtual bool parseQueueLine(const QString &queueListOutput, IdType *queueId, JobState *state); }; } // End namespace #endif // QUEUEPBS_H molequeue-0.9.0/molequeue/app/queues/queueuit.cpp000066400000000000000000000572601323436134600222020ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "queueuit.h" #include "uit/kerberoscredentials.h" #include "uit/sslsetup.h" #include "uit/authenticator.h" #include "uit/userhostassoclist.h" #include "uit/sessionmanager.h" #include "uit/requests.h" #include "uit/jobsubmissioninfo.h" #include "uit/directoryupload.h" #include "uit/directorydownload.h" #include "uit/directorydelete.h" #include "uit/directorycreate.h" #include "job.h" #include "jobmanager.h" #include "logentry.h" #include "logger.h" #include "program.h" #include "uitqueuewidget.h" #include "server.h" #include "credentialsdialog.h" #include "logger.h" #include "mainwindow.h" #include "filesystemtools.h" #include #include #include #include #include #include #include #include #include namespace MoleQueue { const QString QueueUit::clientId = "0adc5b59-5827-4331-a544-5ba7922ec2b8"; QueueUit::QueueUit(QueueManager *parentObject) : QueueRemote("ezHPC UIT", parentObject), m_uitSession(NULL), m_kerberosRealm("HPCMP.HPC.MIL"), m_hostID(-1), m_dialogParent(NULL), m_isCheckingQueue(false) { setLaunchScriptName("job.uit"); // ensure SSL certificates are loaded Uit::SslSetup::init(); m_launchTemplate = "#!/bin/sh\n" "#\n" "# Sample job script provided by MoleQueue.\n" "#PBS -l procs=1\n" "#PBS -l walltime=01:00:00\n" "#PBS -A \n" "#PBS -q debug\n" "#\n" "$$programExecution$$\n"; } QueueUit::~QueueUit() { } bool QueueUit::writeJsonSettings(QJsonObject &json, bool exportOnly, bool includePrograms) const { if (!QueueRemote::writeJsonSettings(json, exportOnly, includePrograms)) return false; json["kerberosUserName"] = m_kerberosUserName; json["kerberosRealm"] = m_kerberosRealm; json["hostName"] = m_hostName; json["hostID"] = QString::number(m_hostID); return true; } bool QueueUit::readJsonSettings(const QJsonObject &json, bool importOnly, bool includePrograms) { // Validate JSON: if (!json["kerberosUserName"].isString() || !json["kerberosRealm"].isString() || !json["hostName"].isString() || !json["hostID"].isString()) { Logger::logError(tr("Error reading queue settings: Invalid format:\n%1") .arg(QJsonDocument(json).toJson().constData())); return false; } if (!QueueRemote::readJsonSettings(json, importOnly, includePrograms)) return false; m_kerberosUserName = json["kerberosUserName"].toString(); m_kerberosRealm = json["kerberosRealm"].toString(); m_hostName = json["hostName"].toString(); m_hostID = json["hostID"].toString().toLongLong(); return true; } bool QueueUit::testConnection(QWidget *parentObject) { uitSession()->authenticate(this, SLOT(testConnectionComplete(const QString&)), this, SLOT(testConnectionError(const QString&))); m_dialogParent = parentObject; return true; } void QueueUit::testConnectionComplete(const QString &token) { QMessageBox::information(m_dialogParent, tr("Success"), tr("Connection to UIT succeeded!")); Q_UNUSED(token); } void QueueUit::testConnectionError(const QString &errorMessage) { QMessageBox::critical(m_dialogParent, tr("UIT Error"), errorMessage); } AbstractQueueSettingsWidget* QueueUit::settingsWidget() { UitQueueWidget *widget = new UitQueueWidget (this); return widget; } void QueueUit::createRemoteDirectory(Job job) { QString remoteDir = QString("%1/%2").arg(m_workingDirectoryBase) .arg(job.moleQueueId()); Uit::DirectoryCreate *create = new Uit::DirectoryCreate(uitSession(), this); create->setHostId(m_hostID); create->setUserName(m_kerberosUserName); create->setJob(job); create->setDirectory(remoteDir); connect(create, SIGNAL(finished()), this, SLOT(remoteDirectoryCreated())); connect(create, SIGNAL(error(const QString &)), this, SLOT(createRemoteDirectoryError(const QString&))); create->start(); } void QueueUit::createRemoteDirectoryError(const QString &errorString) { Uit::DirectoryCreate *createRequest = qobject_cast(sender()); if (!createRequest) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not UitDirectoryCreate!")); return; } createRequest->deleteLater(); Job job = createRequest->job(); Logger::logWarning(tr("Cannot create remote directory %1.\n" "%2") .arg(createRequest->directory()).arg(errorString), job.moleQueueId()); // Retry submission: if (addJobFailure(job.moleQueueId())) m_pendingSubmission.append(job.moleQueueId()); job.setJobState(MoleQueue::Error); emit uitMethodError(errorString); } void QueueUit::remoteDirectoryCreated() { Uit::DirectoryCreate *createRequest = qobject_cast(sender()); if (!createRequest) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not UitDirectoryCreate!")); return; } createRequest->deleteLater(); uploadInputFilesToHost(createRequest->job()); } void QueueUit::copyInputFilesToHost(Job job) { QString localDir = job.localWorkingDirectory(); QString remoteDir = QDir::cleanPath((m_workingDirectoryBase)); Uit::StatFileRequest *request = new Uit::StatFileRequest(uitSession(), this); request->setJob(job); request->setHostId(m_hostID); request->setUserName(m_kerberosUserName); request->setFilename(remoteDir); connect(request, SIGNAL(finished()), this, SLOT(processStatFileRequest())); connect(request, SIGNAL(error(const QString &)), this, SLOT(copyInputFilesToHostError(const QString &))); request->submit(); } void QueueUit::processStatFileRequest() { Uit::StatFileRequest *request = qobject_cast(sender()); if (!request) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not StatFileRequest!")); return; } request->deleteLater(); Job job = request->job(); uploadInputFilesToHost(job); } void QueueUit::uploadInputFilesToHost(Job job) { QString localDir = job.localWorkingDirectory(); QString remoteDir = QDir::cleanPath(QString("%1/%2") .arg(m_workingDirectoryBase) .arg(job.moleQueueId())); Uit::DirectoryUpload *uploader = new Uit::DirectoryUpload(uitSession(), this); uploader->setHostId(m_hostID); uploader->setUserName(m_kerberosUserName); uploader->setLocalPath(localDir); uploader->setRemotePath(remoteDir); uploader->setJob(job); connect(uploader, SIGNAL(finished()), this, SLOT(inputFilesCopied())); connect(uploader, SIGNAL(error(const QString &)), this, SLOT(copyInputFilesToHostError(const QString &))); uploader->start(); } void QueueUit::copyInputFilesToHostError(const QString &errorString) { Uit::StatFileRequest *request = qobject_cast(sender()); if (!request) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not UitDirUploader!")); return; } request->deleteLater(); Job job = request->job(); if (errorString.contains(Uit::FileSystemOperation::noSuchFileOrDir)) createRemoteDirectory(job); else { Logger::logError(tr("UIT error copying input files: '%1'").arg(errorString), job.moleQueueId()); if (addJobFailure(job.moleQueueId())) m_pendingSubmission.append(job.moleQueueId()); job.setJobState(MoleQueue::Error); emit uitMethodError(errorString); } } void QueueUit::inputFilesCopied() { Uit::DirectoryUpload *uploader = qobject_cast(sender()); if (!uploader) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not UitDirUploader!")); return; } uploader->deleteLater(); Job job = uploader->job(); submitJobToRemoteQueue(job); } void QueueUit::submitJobToRemoteQueue(Job job) { Uit::SubmitBatchScriptJobRequest *request = new Uit::SubmitBatchScriptJobRequest(uitSession(), this); const Program *prog = lookupProgram(job.program()); // TODO Should be pust to queue?? QString launchString = prog->launchTemplate(); replaceKeywords(launchString, job); QString workingDir = QString("%1/%2").arg(m_workingDirectoryBase) .arg(job.moleQueueId()); request->setHostId(hostId()); request->setUserName(m_kerberosUserName); request->setJob(job); request->setBatchScript(launchString); request->setWorkingDir(workingDir); connect(request, SIGNAL(finished()), this, SLOT(jobSubmittedToRemoteQueue())); connect(request, SIGNAL(error(const QString&)), this, SLOT(jobSubmissionError(const QString&))); request->submit(); } void QueueUit::jobSubmittedToRemoteQueue() { Uit::SubmitBatchScriptJobRequest *request = qobject_cast(sender()); if (!request) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not SubmitBatchScriptJobRequest!")); return; } request->deleteLater(); Uit::JobSubmissionInfo info = request->jobSubmissionInfo(); if (!info.isValid()) { Logger::logError(tr("Invalid response from UIT server: %1") .arg(info.xml())); } Job job = request->job(); if (!info.stderr().isEmpty()) { Logger::logWarning(tr("Could not submit job to remote UIT queue on %1:\n" "stderr: %2") .arg(m_hostName) .arg(info.stderr())); // Retry submission: if (addJobFailure(job.moleQueueId())) m_pendingSubmission.append(job.moleQueueId()); job.setJobState(MoleQueue::Error); return; } IdType queueId = request->jobSubmissionInfo().jobNumber(); job.setJobState(MoleQueue::Submitted); clearJobFailures(job.moleQueueId()); job.setQueueId(queueId); m_jobs.insert(queueId, job.moleQueueId()); } void QueueUit::jobSubmissionError(const QString &errorString) { Uit::SubmitBatchScriptJobRequest *request = qobject_cast(sender()); if (!request) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not SubmitBatchScriptJobRequest!")); return; } request->deleteLater(); Job job = request->job(); Logger::logWarning(tr("Could not submit job to remote UIT queue on %1:\n" "%2") .arg(m_hostName) .arg(errorString)); // Retry submission: if (addJobFailure(job.moleQueueId())) m_pendingSubmission.append(job.moleQueueId()); job.setJobState(MoleQueue::Error); emit uitMethodError(errorString); } void QueueUit::requestQueueUpdate() { if (m_isCheckingQueue) return; if (m_jobs.isEmpty()) return; m_isCheckingQueue = true; Uit::GetJobsForHostForUserByNumDaysRequest *request = new Uit::GetJobsForHostForUserByNumDaysRequest(uitSession(), this); request->setHostId(m_hostID); request->setSearchUser(m_kerberosUserName); request->setUserName(m_kerberosUserName); // What should we set this too??? request->setNumDays(1); connect(request, SIGNAL(finished()), this, SLOT(handleQueueUpdate())); connect(request, SIGNAL(error(const QString&)), this, SLOT(requestQueueUpdateError(const QString&))); request->submit(); } void QueueUit::requestQueueUpdateError(const QString &errorString) { Uit::GetJobsForHostForUserByNumDaysRequest *request = qobject_cast(sender()); if (!request) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not GetJobsForHostForUserByNumDaysRequest!")); return; } request->deleteLater(); Logger::logWarning(tr("Error requesting queue data: %1)").arg(errorString)); m_isCheckingQueue = false; emit uitMethodError(errorString); } void QueueUit::handleQueueUpdate() { Uit::GetJobsForHostForUserByNumDaysRequest *request = qobject_cast(sender()); if (!request) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not GetJobsForHostForUserByNumDaysRequest!")); return; } request->deleteLater(); Uit::JobEventList jobEvents = request->jobEventList(m_jobs.keys()); if (!jobEvents.isValid()) { Logger::logError(tr("Invalid response from UIT server: %1") .arg(jobEvents.xml())); } handleQueueUpdate(jobEvents.jobEvents()); } bool lessThan(const Uit::JobEvent &j1, const Uit::JobEvent &j2) { return j1.eventTime() < j2.eventTime(); } void QueueUit::handleQueueUpdate(const QList &jobEvents) { QList justFinished; QList queueIds = m_jobs.keys(); QMap > eventMap; // Filter JobEvents by jobId foreach(const Uit::JobEvent& jobEvent, jobEvents) { QList events; if (!eventMap.contains(jobEvent.jobId())) { eventMap[jobEvent.jobId()] = QList(); } eventMap[jobEvent.jobId()].append(jobEvent); } foreach(IdType queueId, queueIds) { IdType moleQueueId = m_jobs.value(queueId, InvalidId); if (moleQueueId != InvalidId) { // Get pointer to jobmanager to lookup job if (!m_server) { Logger::logError(tr("Queue '%1' cannot locate Server instance!") .arg(m_name), moleQueueId); m_isCheckingQueue = false; return; } Job job = m_server->jobManager()->lookupJobByMoleQueueId(moleQueueId); if (!job.isValid()) { Logger::logError(tr("Queue '%1' Cannot update invalid Job reference!") .arg(m_name), moleQueueId); continue; } QList events = eventMap[queueId]; // If there are no events then we assume it has finished. if (events.isEmpty()) { beginFinalizeJob(queueId); continue; } Uit::JobEvent lastEvent = events.last(); JobState currentState = jobEventToJobState(lastEvent); if (currentState != job.jobState()) job.setJobState(currentState); } } m_isCheckingQueue = false; } void QueueUit::beginFinalizeJob(IdType queueId) { IdType moleQueueId = m_jobs.value(queueId, InvalidId); if (moleQueueId == InvalidId) return; m_jobs.remove(queueId); // Lookup job if (!m_server) return; Job job = m_server->jobManager()->lookupJobByMoleQueueId(moleQueueId); if (!job.isValid()) return; finalizeJobCopyFromServer(job); } void QueueUit::finalizeJobCopyFromServer(Job job) { if (!job.retrieveOutput() || (job.cleanLocalWorkingDirectory() && job.outputDirectory().isEmpty()) ) { // Jump to next step finalizeJobCopyToCustomDestination(job); return; } QString localDir = job.localWorkingDirectory(); QString remoteDir = QString("%1/%2").arg(m_workingDirectoryBase).arg(job.moleQueueId()); Uit::DirectoryDownload *downloader = new Uit::DirectoryDownload(uitSession(), this); downloader->setJob(job); downloader->setHostId(m_hostID); downloader->setUserName(m_kerberosUserName); downloader->setRemotePath(remoteDir); downloader->setLocalPath(localDir); connect(downloader, SIGNAL(finished()), this, SLOT(finalizeJobOutputCopiedFromServer())); connect(downloader, SIGNAL(error(const QString &)), this, SLOT(finalizeJobCopyFromServerError(const QString &))); downloader->start(); } void QueueUit::finalizeJobCopyFromServerError(const QString &errorString) { Uit::DirectoryDownload *downloader = qobject_cast(sender()); if (!downloader) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not UitDirDownloader!")); return; } downloader->deleteLater(); Job job = downloader->job(); Logger::logError(tr("Error copy file from server: %1").arg(errorString), job.moleQueueId()); job.setJobState(MoleQueue::Error); emit uitMethodError(errorString); } void QueueUit::finalizeJobOutputCopiedFromServer() { Uit::DirectoryDownload *downloader = qobject_cast(sender()); if (!downloader) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not UitDirDownloader!")); return; } downloader->deleteLater(); finalizeJobCopyToCustomDestination(downloader->job()); } // TODO This can probably move to the super class ?? void QueueUit::finalizeJobCopyToCustomDestination(Job job) { // Skip to next step if needed if (job.outputDirectory().isEmpty() || job.outputDirectory() == job.localWorkingDirectory()) { finalizeJobCleanup(job); return; } // The copy function will throw errors if needed. if (!FileSystemTools::recursiveCopyDirectory(job.localWorkingDirectory(), job.outputDirectory())) { job.setJobState(MoleQueue::Error); return; } finalizeJobCleanup(job); } void QueueUit::finalizeJobCleanup(Job job) { if (job.cleanLocalWorkingDirectory()) cleanLocalDirectory(job); if (job.cleanRemoteFiles()) cleanRemoteDirectory(job); job.setJobState(MoleQueue::Finished); } void QueueUit::cleanRemoteDirectory(Job job) { QString remoteDir = QDir::cleanPath( QString("%1/%2").arg(m_workingDirectoryBase).arg(job.moleQueueId())); // Check that the remoteDir is not just "/" due to another bug. if (remoteDir.simplified() == "/") { Logger::logError(tr("Refusing to clean remote directory %1 -- an internal " "error has occurred.").arg(remoteDir), job.moleQueueId()); return; } Uit::DirectoryDelete *deleter = new Uit::DirectoryDelete(uitSession(), this); deleter->setHostId(m_hostID); deleter->setUserName(m_kerberosUserName); deleter->setJob(job); deleter->setDirectory(remoteDir); connect(deleter, SIGNAL(finished()), this, SLOT(remoteDirectoryCleaned())); connect(deleter, SIGNAL(error(const QString &)), this, SLOT(cleanRemoteDirectoryError(const QString &))); } void QueueUit::cleanRemoteDirectoryError(const QString &errorString) { Uit::DirectoryDelete *deleter = qobject_cast(sender()); if (!deleter) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not UitDirDeleter!")); return; } deleter->deleteLater(); Job job = deleter->job(); Logger::logError(tr("Error clearing remote directory '%1.\n" "%2").arg(deleter->directory()).arg(errorString), job.moleQueueId()); job.setJobState(MoleQueue::Error); emit uitMethodError(errorString); } void QueueUit::remoteDirectoryCleaned() { Uit::DirectoryDelete *deleter = qobject_cast(sender()); if (!deleter) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not UitDirDeleter!")); return; } deleter->deleteLater(); } void QueueUit::beginKillJob(Job job) { Uit::CancelJobRequest *request = new Uit::CancelJobRequest(uitSession(), this); request->setHostId(m_hostID); request->setUserName(m_kerberosUserName); request->setJob(job); connect(request, SIGNAL(finished()), this, SLOT(endKillJob())); connect(request, SIGNAL(error(const QString&)), this, SLOT(killJobError(const QString&))); request->submit(); } void QueueUit::killJobError(const QString &errorString) { Uit::CancelJobRequest *request = qobject_cast(sender()); if (!request) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not CancelJobRequest!")); return; } request->deleteLater(); Job job = request->job(); Logger::logWarning(tr("Error canceling job (mqid=%1, queueid=%2) %3 ") .arg(job.moleQueueId()).arg(job.queueId()) .arg(errorString)); emit uitMethodError(errorString); } void QueueUit::endKillJob() { Uit::CancelJobRequest *request = qobject_cast(sender()); if (!request) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not CancelJobRequest!")); return; } request->deleteLater(); Job job = request->job(); job.setJobState(MoleQueue::Canceled); } void QueueUit::getUserHostAssoc() { Uit::GetUserHostAssocRequest *request = new Uit::GetUserHostAssocRequest(uitSession(), this); connect(request, SIGNAL(finished()), this, SLOT(getUserHostAssocComplete())); connect(request, SIGNAL(error(const QString&)), this, SLOT(requestError(const QString&))); request->submit(); } void QueueUit::getUserHostAssocComplete() { Uit::GetUserHostAssocRequest *request = qobject_cast(sender()); if (!request) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not GetUserHostAssocRequest!")); return; } request->deleteLater(); Uit::UserHostAssocList userHostAssoc = request->userHostAssocList(); if (!userHostAssoc.isValid()) { Logger::logError(tr("Invalid response from UIT server: %1") .arg(userHostAssoc.xml())); return; } emit userHostAssocList(userHostAssoc); } Uit::Session * QueueUit::uitSession() { if (!m_uitSession) m_uitSession = Uit::SessionManager::instance()->session(m_kerberosUserName, m_kerberosRealm); return m_uitSession; } void QueueUit::requestError(const QString &errorMessage) { Uit::Request *request = qobject_cast(sender()); if (!request) { Logger::logWarning(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not UitRequest!")); return; } request->deleteLater(); emit uitMethodError(errorMessage); } JobState QueueUit::jobEventToJobState(Uit::JobEvent jobEvent) { QString jobStatus = jobEvent.jobStatusText().trimmed(); if (jobStatus.length() != 1) { Logger::logError(tr("Unrecognized jobStatus: %1").arg(jobStatus)); return MoleQueue::Error; } JobState jobState = Unknown; char state = jobStatus.toLower()[0].toLatin1(); switch (state) { case 'r': case 'e': case 'c': jobState = RunningRemote; break; case 'q': case 'h': case 't': case 'w': case 's': jobState = QueuedRemote; break; default: Logger::logWarning(tr("Unrecognized queue state '%1'.").arg(state)); } return jobState; } } // End namespace molequeue-0.9.0/molequeue/app/queues/queueuit.h000066400000000000000000000114061323436134600216370ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef QUEUEUIT_H #define QUEUEUIT_H #include "wsdl_uitapi.h" #include "credentialsdialog.h" #include "uit/authresponseprocessor.h" #include "uit/authenticateresponse.h" #include "uit/session.h" #include "uit/userhostassoclist.h" #include "uit/jobevent.h" #include "remote.h" #include #include #include #include class QueueUitTest; namespace MoleQueue { /** @brief QueueRemote subclass for interacting with a remote queue * over UIT. */ class QueueUit : public QueueRemote { Q_OBJECT public: explicit QueueUit(QueueManager *parentManager = 0); ~QueueUit(); virtual QString typeName() const { return "ezHPC UIT"; } bool writeJsonSettings(QJsonObject &json, bool exportOnly, bool includePrograms) const; bool readJsonSettings(const QJsonObject &json, bool importOnly, bool includePrograms); AbstractQueueSettingsWidget* settingsWidget(); /** * @return The Kerberos username. */ QString kerberosUserName() { return m_kerberosUserName; } /** * Set the Kereros username. */ void setKerberosUserName(const QString &userName) { m_kerberosUserName = userName; } /** * @return The Kerberos realm. */ QString kerberosRealm() { return m_kerberosRealm; } /** * Set the Kerberos realm. */ void setKerberosRealm(const QString &realm) { m_kerberosRealm = realm; } QString hostName() const { return m_hostName; } void setHostName(const QString &host) { m_hostName = host; } qint64 hostId() { return m_hostID; } void setHostID(qint64 id) { m_hostID = id; } /** * Test the connection to UIT. */ bool testConnection(QWidget *parentObject); // /** // * Submit sleep job to queue. // */ // void sleepTest(QWidget *parentObject); // Needed for testing friend class ::QueueUitTest; static const QString clientId; signals: void uitMethodError(const QString &errorString); void userHostAssocList(const Uit::UserHostAssocList &list); public slots: void requestQueueUpdate(); protected slots: void createRemoteDirectory(MoleQueue::Job job); void createRemoteDirectoryError(const QString &errorString); void remoteDirectoryCreated(); void copyInputFilesToHost(MoleQueue::Job job); void copyInputFilesToHostError(const QString &erroString); void inputFilesCopied(); void uploadInputFilesToHost(Job job); void processStatFileRequest(); void submitJobToRemoteQueue(MoleQueue::Job job); void jobSubmittedToRemoteQueue(); void jobSubmissionError(const QString &errorString); void handleQueueUpdate(); void handleQueueUpdate(const QList &jobEvents); void requestQueueUpdateError(const QString&); //void beginJobSubmission(Job job); void beginFinalizeJob(MoleQueue::IdType queueId); void finalizeJobCopyFromServer(MoleQueue::Job job); void finalizeJobOutputCopiedFromServer(); void finalizeJobCopyFromServerError(const QString &errorString); void finalizeJobCopyToCustomDestination(MoleQueue::Job job); void finalizeJobCleanup(MoleQueue::Job job); void cleanRemoteDirectory(MoleQueue::Job job); void cleanRemoteDirectoryError(const QString &errorString); void remoteDirectoryCleaned(); void beginKillJob(MoleQueue::Job job); void killJobError(const QString &errorString); void endKillJob(); /** * Called when test authentication is complete * * @param The session token. */ void testConnectionComplete(const QString &token); /** * Called when test authentication produces an error * * @param The error message. */ void testConnectionError(const QString &errorMessage); private: Uit::Session *m_uitSession; QString m_kerberosUserName; QString m_kerberosRealm; QString m_hostName; qint64 m_hostID; UitapiService m_uit; QWidget *m_dialogParent; bool m_isCheckingQueue; Uit::Session * uitSession(); private slots: void getUserHostAssoc(); void getUserHostAssocComplete(); void requestError(const QString &errorMessage); JobState jobEventToJobState(Uit::JobEvent event); }; } // End namespace #endif // QUEUEUIT_H molequeue-0.9.0/molequeue/app/queues/remote.cpp000066400000000000000000000216261323436134600216240ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "remote.h" #include "../filesystemtools.h" #include "../job.h" #include "../jobmanager.h" #include "../logentry.h" #include "../logger.h" #include "../program.h" #include "../remotequeuewidget.h" #include "../server.h" #include #include #include #include namespace MoleQueue { QueueRemote::QueueRemote(const QString &queueName, QueueManager *parentObject) : Queue(queueName, parentObject), m_checkForPendingJobsTimerId(-1), m_queueUpdateInterval(DEFAULT_REMOTE_QUEUE_UPDATE_INTERVAL), m_defaultMaxWallTime(DEFAULT_MAX_WALLTIME) { // Set remote queue check timer. m_checkQueueTimerId = startTimer(m_queueUpdateInterval * 60000); // Check for jobs to submit every 5 seconds m_checkForPendingJobsTimerId = startTimer(5000); } QueueRemote::~QueueRemote() { } bool QueueRemote::writeJsonSettings(QJsonObject &json, bool exportOnly, bool includePrograms) const { if (!Queue::writeJsonSettings(json, exportOnly, includePrograms)) return false; if (!exportOnly) json.insert("workingDirectoryBase", m_workingDirectoryBase); json.insert("queueUpdateInterval", static_cast(m_queueUpdateInterval)); json.insert("defaultMaxWallTime", static_cast(m_defaultMaxWallTime)); return true; } bool QueueRemote::readJsonSettings(const QJsonObject &json, bool importOnly, bool includePrograms) { // Validate JSON: if ((!importOnly && !json.value("workingDirectoryBase").isString()) || !json.value("queueUpdateInterval").isDouble() || !json.value("defaultMaxWallTime").isDouble()) { Logger::logError(tr("Error reading queue settings: Invalid format:\n%1") .arg(QString(QJsonDocument(json).toJson()))); return false; } if (!Queue::readJsonSettings(json, importOnly, includePrograms)) return false; if (!importOnly) m_workingDirectoryBase = json.value("workingDirectoryBase").toString(); m_queueUpdateInterval = static_cast(json.value("queueUpdateInterval").toDouble() + 0.5); m_defaultMaxWallTime = static_cast(json.value("defaultMaxWallTime").toDouble() + 0.5); return true; } void QueueRemote::setQueueUpdateInterval(int interval) { if (interval == m_queueUpdateInterval) return; m_queueUpdateInterval = interval; killTimer(m_checkQueueTimerId); m_checkQueueTimerId = startTimer(m_queueUpdateInterval * 60000); requestQueueUpdate(); } void QueueRemote::replaceKeywords(QString &launchScript, const Job &job, bool addNewline) { // If a valid walltime is set, replace all occurances with the appropriate // string: int wallTime = job.maxWallTime(); int hours = wallTime / 60; int minutes = wallTime % 60; if (wallTime > 0) { launchScript.replace("$$$maxWallTime$$$", QString("%1:%2:00") .arg(hours, 2, 10, QChar('0')) .arg(minutes, 2, 10, QChar('0'))); } // Otherwise, erase all lines containing the keyword else { QRegExp expr("\\n[^\\n]*\\${3,3}maxWallTime\\${3,3}[^\\n]*\\n"); launchScript.replace(expr, "\n"); } if (wallTime <= 0) { wallTime = defaultMaxWallTime(); hours = wallTime / 60; minutes = wallTime % 60; } launchScript.replace("$$maxWallTime$$", QString("%1:%2:00") .arg(hours, 2, 10, QChar('0')) .arg(minutes, 2, 10, QChar('0'))); Queue::replaceKeywords(launchScript, job, addNewline); } bool QueueRemote::submitJob(Job job) { if (job.isValid()) { m_pendingSubmission.append(job.moleQueueId()); job.setJobState(MoleQueue::Accepted); return true; } Logger::logError(tr("Refusing to submit job to Queue '%1': Job object is " "invalid.").arg(m_name), job.moleQueueId()); return false; } void QueueRemote::killJob(Job job) { if (!job.isValid()) return; int pendingIndex = m_pendingSubmission.indexOf(job.moleQueueId()); if (pendingIndex >= 0) { m_pendingSubmission.removeAt(pendingIndex); job.setJobState(MoleQueue::Canceled); return; } if (job.queue() == m_name && job.queueId() != InvalidId && m_jobs.value(job.queueId()) == job.moleQueueId()) { m_jobs.remove(job.queueId()); beginKillJob(job); return; } Logger::logWarning(tr("Queue '%1' requested to kill unknown job that belongs " "to queue '%2', queue id '%3'.").arg(m_name) .arg(job.queue()).arg(idTypeToString(job.queueId())), job.moleQueueId()); job.setJobState(MoleQueue::Canceled); } void QueueRemote::submitPendingJobs() { if (m_pendingSubmission.isEmpty()) return; // lookup job manager: JobManager *jobManager = NULL; if (m_server) jobManager = m_server->jobManager(); if (!jobManager) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Cannot locate server JobManager!")); return; } foreach (const IdType moleQueueId, m_pendingSubmission) { Job job = jobManager->lookupJobByMoleQueueId(moleQueueId); // Kick off the submission process... beginJobSubmission(job); } m_pendingSubmission.clear(); } void QueueRemote::beginJobSubmission(Job job) { if (!writeInputFiles(job)) { Logger::logError(tr("Error while writing input files."), job.moleQueueId()); job.setJobState(Error); return; } // Attempt to copy the files via scp first. Only call mkdir on the remote // working directory if the scp call fails. copyInputFilesToHost(job); } void QueueRemote::beginFinalizeJob(IdType queueId) { IdType moleQueueId = m_jobs.value(queueId, InvalidId); if (moleQueueId == InvalidId) return; m_jobs.remove(queueId); // Lookup job if (!m_server) return; Job job = m_server->jobManager()->lookupJobByMoleQueueId(moleQueueId); if (!job.isValid()) return; finalizeJobCopyFromServer(job); } void QueueRemote::finalizeJobCopyToCustomDestination(Job job) { // Skip to next step if needed if (job.outputDirectory().isEmpty() || job.outputDirectory() == job.localWorkingDirectory()) { finalizeJobCleanup(job); return; } // The copy function will throw errors if needed. if (!FileSystemTools::recursiveCopyDirectory(job.localWorkingDirectory(), job.outputDirectory())) { Logger::logError(tr("Cannot copy '%1' -> '%2'.") .arg(job.localWorkingDirectory(), job.outputDirectory()), job.moleQueueId()); job.setJobState(MoleQueue::Error); return; } finalizeJobCleanup(job); } void QueueRemote::finalizeJobCleanup(Job job) { if (job.cleanLocalWorkingDirectory()) cleanLocalDirectory(job); if (job.cleanRemoteFiles()) cleanRemoteDirectory(job); job.setJobState(MoleQueue::Finished); } void QueueRemote::jobAboutToBeRemoved(const Job &job) { m_pendingSubmission.removeOne(job.moleQueueId()); Queue::jobAboutToBeRemoved(job); } void QueueRemote::removeStaleJobs() { if (m_server) { if (JobManager *jobManager = m_server->jobManager()) { QList staleQueueIds; for (QMap::const_iterator it = m_jobs.constBegin(), it_end = m_jobs.constEnd(); it != it_end; ++it) { if (jobManager->lookupJobByMoleQueueId(it.value()).isValid()) continue; staleQueueIds << it.key(); Logger::logError(tr("Job with MoleQueue id %1 is missing, but the Queue" " '%2' is still holding a reference to it. Please " "report this bug and check if the job needs to be " "resubmitted.").arg(it.value()).arg(name()), it.value()); } foreach (IdType queueId, staleQueueIds) m_jobs.remove(queueId); } } } void QueueRemote::timerEvent(QTimerEvent *theEvent) { if (theEvent->timerId() == m_checkQueueTimerId) { theEvent->accept(); removeStaleJobs(); if (!m_jobs.isEmpty()) requestQueueUpdate(); return; } else if (theEvent->timerId() == m_checkForPendingJobsTimerId) { theEvent->accept(); submitPendingJobs(); return; } QObject::timerEvent(theEvent); } } // End namespace molequeue-0.9.0/molequeue/app/queues/remote.h000066400000000000000000000113121323436134600212600ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef QUEUEREMOTE_H #define QUEUEREMOTE_H #include "../queue.h" class QTimer; namespace MoleQueue { class QueueManager; /// @brief abstract Queue subclass for interacting with a generic Remote queue. class QueueRemote : public Queue { Q_OBJECT public: explicit QueueRemote(const QString &queueName = "AbstractRemote", QueueManager *parentManager = 0); ~QueueRemote(); bool writeJsonSettings(QJsonObject &json, bool exportOnly, bool includePrograms) const; bool readJsonSettings(const QJsonObject &json, bool importOnly, bool includePrograms); virtual AbstractQueueSettingsWidget* settingsWidget() = 0; void setWorkingDirectoryBase(const QString &base) { m_workingDirectoryBase = base; } QString workingDirectoryBase() const { return m_workingDirectoryBase; } /** Time between remote queue updates in minutes. */ void setQueueUpdateInterval(int i); /** Time between remote queue updates in minutes. */ int queueUpdateInterval() const { return m_queueUpdateInterval; } /** * @brief setDefaultMaxWallTime Set the default walltime limit (in minutes) * for jobs on this queue. This value will be used if the job's * Job::maxWallTime() method returns a value <= 0. Default is one day. * @param time walltime limit in minutes * @see Job::maxWallTime() */ void setDefaultMaxWallTime(int time) { m_defaultMaxWallTime = time; } /** * @brief defaultMaxWallTime Get the default walltime limit (in minutes) * for jobs on this queue. This value will be used if the job's * Job::maxWallTime() method returns a value <= 0. Default is one day. * @return walltime limit in minutes * @see Job::maxWallTime() */ int defaultMaxWallTime() const { return m_defaultMaxWallTime; } /// Reimplemented from Queue::replaceKeywords void replaceKeywords(QString &launchScript, const Job &job, bool addNewline = true); public slots: bool submitJob(MoleQueue::Job job); void killJob(MoleQueue::Job job); virtual void requestQueueUpdate() = 0; protected slots: virtual void submitPendingJobs(); /// Main entry point into the job submission pipeline virtual void beginJobSubmission(MoleQueue::Job job); virtual void createRemoteDirectory(MoleQueue::Job job) = 0; virtual void remoteDirectoryCreated() = 0; virtual void copyInputFilesToHost(MoleQueue::Job job) = 0; virtual void inputFilesCopied() = 0; virtual void submitJobToRemoteQueue(MoleQueue::Job job) = 0; virtual void jobSubmittedToRemoteQueue() = 0; virtual void handleQueueUpdate() = 0; virtual void beginFinalizeJob(MoleQueue::IdType queueId) = 0; virtual void finalizeJobCopyFromServer(MoleQueue::Job job) = 0; virtual void finalizeJobOutputCopiedFromServer() = 0; virtual void finalizeJobCopyToCustomDestination(MoleQueue::Job job) = 0; virtual void finalizeJobCleanup(MoleQueue::Job job); virtual void cleanRemoteDirectory(MoleQueue::Job job) = 0; virtual void remoteDirectoryCleaned() = 0; /// Reimplemented from Queue void jobAboutToBeRemoved(const MoleQueue::Job &job); virtual void beginKillJob(MoleQueue::Job job) = 0; virtual void endKillJob() = 0; protected: /** * Check for any jobs that are not present in the JobManager but * are still in this object's internal data structures. This may be the result * of an improper shut down when state is serialized inconsistently. If any * such jobs are found, they are removed from the internal structures and an * Error is emitted. */ virtual void removeStaleJobs(); /// Reimplemented to monitor queue events. virtual void timerEvent(QTimerEvent *theEvent); int m_checkQueueTimerId; /// MoleQueue ids of jobs that have been accepted but not submitted. QList m_pendingSubmission; int m_checkForPendingJobsTimerId; /// Time between remote queue updates in minutes. int m_queueUpdateInterval; /// Default maximum walltime limit for jobs on this queue in minutes. int m_defaultMaxWallTime; QString m_workingDirectoryBase; }; } // End namespace #endif // QUEUEREMOTE_H molequeue-0.9.0/molequeue/app/queues/remotessh.cpp000066400000000000000000000511341323436134600223370ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "remotessh.h" #include "../filesystemtools.h" #include "../job.h" #include "../jobmanager.h" #include "../logentry.h" #include "../logger.h" #include "../program.h" #include "../remotequeuewidget.h" #include "../server.h" #include "../sshcommandfactory.h" #include #include #include #include namespace MoleQueue { QueueRemoteSsh::QueueRemoteSsh(const QString &queueName, QueueManager *parentObject) : QueueRemote(queueName, parentObject), m_sshExecutable(SshCommandFactory::defaultSshCommand()), m_scpExecutable(SshCommandFactory::defaultScpCommand()), m_sshPort(22), m_isCheckingQueue(false) { // Check for jobs to submit every 5 seconds m_checkForPendingJobsTimerId = startTimer(5000); // Always allow m_requestQueueCommand to return 0 m_allowedQueueRequestExitCodes.append(0); } QueueRemoteSsh::~QueueRemoteSsh() { } bool QueueRemoteSsh::writeJsonSettings(QJsonObject &json, bool exportOnly, bool includePrograms) const { if (!QueueRemote::writeJsonSettings(json, exportOnly, includePrograms)) return false; json.insert("submissionCommand", m_submissionCommand); json.insert("requestQueueCommand", m_requestQueueCommand); json.insert("killCommand", m_killCommand); json.insert("hostName", m_hostName); json.insert("sshPort", static_cast(m_sshPort)); if (!exportOnly) { json.insert("sshExecutable", m_sshExecutable); json.insert("scpExecutable", m_scpExecutable); json.insert("userName", m_userName); json.insert("identityFile", m_identityFile); } return true; } bool QueueRemoteSsh::readJsonSettings(const QJsonObject &json, bool importOnly, bool includePrograms) { // Validate JSON if (!json.value("submissionCommand").isString() || !json.value("requestQueueCommand").isString() || !json.value("killCommand").isString() || !json.value("hostName").isString() || !json.value("sshPort").isDouble() || (!importOnly && ( !json.value("sshExecutable").isString() || !json.value("scpExecutable").isString() || !json.value("userName").isString() || !json.value("identityFile").isString()))) { Logger::logError(tr("Error reading queue settings: Invalid format:\n%1") .arg(QString(QJsonDocument(json).toJson()))); return false; } if (!QueueRemote::readJsonSettings(json, importOnly, includePrograms)) return false; m_submissionCommand = json.value("submissionCommand").toString(); m_requestQueueCommand = json.value("requestQueueCommand").toString(); m_killCommand = json.value("killCommand").toString(); m_hostName = json.value("hostName").toString(); m_sshPort = static_cast(json.value("sshPort").toDouble() + 0.5); if (!importOnly) { m_sshExecutable = json.value("sshExecutable").toString(); m_scpExecutable = json.value("scpExecutable").toString(); m_userName = json.value("userName").toString(); m_identityFile = json.value("identityFile").toString(); } return true; } AbstractQueueSettingsWidget* QueueRemoteSsh::settingsWidget() { RemoteQueueWidget *widget = new RemoteQueueWidget (this); return widget; } void QueueRemoteSsh::createRemoteDirectory(Job job) { // Note that this is just the working directory base -- the job folder is // created by scp. QString remoteDir = QString("%1").arg(m_workingDirectoryBase); SshConnection *conn = newSshConnection(); conn->setData(QVariant::fromValue(job)); connect(conn, SIGNAL(requestComplete()), this, SLOT(remoteDirectoryCreated())); if (!conn->execute(QString("mkdir -p %1").arg(remoteDir))) { Logger::logError(tr("Could not initialize ssh resources: user= '%1'\nhost =" " '%2' port = '%3'") .arg(conn->userName()).arg(conn->hostName()) .arg(conn->portNumber()), job.moleQueueId()); job.setJobState(MoleQueue::Error); conn->deleteLater(); return; } } void QueueRemoteSsh::remoteDirectoryCreated() { SshConnection *conn = qobject_cast(sender()); if (!conn) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not an SshConnection!")); return; } conn->deleteLater(); Job job = conn->data().value(); if (!job.isValid()) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender does not have an associated job!")); return; } if (conn->exitCode() != 0) { Logger::logWarning(tr("Cannot create remote directory '%1@%2:%3'.\n" "Exit code (%4) %5") .arg(conn->userName()).arg(conn->hostName()) .arg(m_workingDirectoryBase).arg(conn->exitCode()) .arg(conn->output()), job.moleQueueId()); // Retry submission: if (addJobFailure(job.moleQueueId())) m_pendingSubmission.append(job.moleQueueId()); job.setJobState(MoleQueue::Error); return; } copyInputFilesToHost(job); } void QueueRemoteSsh::copyInputFilesToHost(Job job) { QString localDir = job.localWorkingDirectory(); QString remoteDir = QDir::cleanPath(QString("%1/%2") .arg(m_workingDirectoryBase) .arg(idTypeToString(job.moleQueueId()))); SshConnection *conn = newSshConnection(); conn->setData(QVariant::fromValue(job)); connect(conn, SIGNAL(requestComplete()), this, SLOT(inputFilesCopied())); if (!conn->copyDirTo(localDir, remoteDir)) { Logger::logError(tr("Could not initialize ssh resources: user= '%1'\nhost =" " '%2' port = '%3'") .arg(conn->userName()).arg(conn->hostName()) .arg(conn->portNumber()), job.moleQueueId()); job.setJobState(MoleQueue::Error); conn->deleteLater(); return; } } void QueueRemoteSsh::inputFilesCopied() { SshConnection *conn = qobject_cast(sender()); if (!conn) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not an SshConnection!")); return; } conn->deleteLater(); Job job = conn->data().value(); if (!job.isValid()) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender does not have an associated job!")); return; } if (conn->exitCode() != 0) { // Check if we just need to make the parent directory if (conn->exitCode() == 1 && conn->output().contains("No such file or directory")) { Logger::logDebugMessage(tr("Remote working directory missing on remote " "host. Creating now..."), job.moleQueueId()); createRemoteDirectory(job); return; } Logger::logWarning(tr("Error while copying input files to remote host:\n" "'%1' --> '%2/'\nExit code (%3) %4") .arg(job.localWorkingDirectory()) .arg(m_workingDirectoryBase) .arg(conn->exitCode()).arg(conn->output()), job.moleQueueId()); // Retry submission: if (addJobFailure(job.moleQueueId())) m_pendingSubmission.append(job.moleQueueId()); job.setJobState(MoleQueue::Error); return; } submitJobToRemoteQueue(job); } void QueueRemoteSsh::submitJobToRemoteQueue(Job job) { const QString command = QString("cd %1/%2 && %3 %4") .arg(m_workingDirectoryBase) .arg(idTypeToString(job.moleQueueId())) .arg(m_submissionCommand) .arg(m_launchScriptName); SshConnection *conn = newSshConnection(); conn->setData(QVariant::fromValue(job)); connect(conn, SIGNAL(requestComplete()), this, SLOT(jobSubmittedToRemoteQueue())); if (!conn->execute(command)) { Logger::logError(tr("Could not initialize ssh resources: user= '%1'\nhost =" " '%2' port = '%3'") .arg(conn->userName()).arg(conn->hostName()) .arg(conn->portNumber()), job.moleQueueId()); job.setJobState(MoleQueue::Error); conn->deleteLater(); return; } } void QueueRemoteSsh::jobSubmittedToRemoteQueue() { SshConnection *conn = qobject_cast(sender()); if (!conn) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not an SshConnection!")); return; } conn->deleteLater(); IdType queueId(0); parseQueueId(conn->output(), &queueId); Job job = conn->data().value(); if (!job.isValid()) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender does not have an associated job!")); return; } if (conn->exitCode() != 0) { Logger::logWarning(tr("Could not submit job to remote queue on %1@%2:%3\n" "%4 %5/%6/%7\nExit code (%8) %9") .arg(conn->userName()).arg(conn->hostName()) .arg(conn->portNumber()).arg(m_submissionCommand) .arg(m_workingDirectoryBase) .arg(idTypeToString(job.moleQueueId())) .arg(m_launchScriptName).arg(conn->exitCode()) .arg(conn->output()), job.moleQueueId()); // Retry submission: if (addJobFailure(job.moleQueueId())) m_pendingSubmission.append(job.moleQueueId()); job.setJobState(MoleQueue::Error); return; } job.setJobState(MoleQueue::Submitted); clearJobFailures(job.moleQueueId()); job.setQueueId(queueId); m_jobs.insert(queueId, job.moleQueueId()); } void QueueRemoteSsh::requestQueueUpdate() { if (m_isCheckingQueue) return; if (m_jobs.isEmpty()) return; m_isCheckingQueue = true; const QString command = generateQueueRequestCommand(); SshConnection *conn = newSshConnection(); connect(conn, SIGNAL(requestComplete()), this, SLOT(handleQueueUpdate())); if (!conn->execute(command)) { Logger::logError(tr("Could not initialize ssh resources: user= '%1'\nhost =" " '%2' port = '%3'") .arg(conn->userName()).arg(conn->hostName()) .arg(conn->portNumber())); conn->deleteLater(); return; } } void QueueRemoteSsh::handleQueueUpdate() { SshConnection *conn = qobject_cast(sender()); if (!conn) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not an SshConnection!")); m_isCheckingQueue = false; return; } conn->deleteLater(); if (!m_allowedQueueRequestExitCodes.contains(conn->exitCode())) { Logger::logWarning(tr("Error requesting queue data (%1 -u %2) on remote " "host %3@%4:%5. Exit code (%6) %7") .arg(m_requestQueueCommand) .arg(m_userName).arg(conn->userName()) .arg(conn->hostName()).arg(conn->portNumber()) .arg(conn->exitCode()).arg(conn->output())); m_isCheckingQueue = false; return; } QStringList output = conn->output().split("\n", QString::SkipEmptyParts); // Get list of submitted queue ids so that we detect when jobs have left // the queue. QList queueIds = m_jobs.keys(); MoleQueue::JobState state; foreach (QString line, output) { IdType queueId; if (parseQueueLine(line, &queueId, &state)) { IdType moleQueueId = m_jobs.value(queueId, InvalidId); if (moleQueueId != InvalidId) { queueIds.removeOne(queueId); // Get pointer to jobmanager to lookup job if (!m_server) { Logger::logError(tr("Queue '%1' cannot locate Server instance!") .arg(m_name), moleQueueId); m_isCheckingQueue = false; return; } Job job = m_server->jobManager()->lookupJobByMoleQueueId(moleQueueId); if (!job.isValid()) { Logger::logError(tr("Queue '%1' Cannot update invalid Job reference!") .arg(m_name), moleQueueId); continue; } job.setJobState(state); } } } // Now copy back any jobs that have left the queue foreach (IdType queueId, queueIds) beginFinalizeJob(queueId); m_isCheckingQueue = false; } void QueueRemoteSsh::beginFinalizeJob(IdType queueId) { IdType moleQueueId = m_jobs.value(queueId, InvalidId); if (moleQueueId == InvalidId) return; m_jobs.remove(queueId); // Lookup job if (!m_server) return; Job job = m_server->jobManager()->lookupJobByMoleQueueId(moleQueueId); if (!job.isValid()) return; finalizeJobCopyFromServer(job); } void QueueRemoteSsh::finalizeJobCopyFromServer(Job job) { if (!job.retrieveOutput() || (job.cleanLocalWorkingDirectory() && job.outputDirectory().isEmpty()) ) { // Jump to next step finalizeJobCopyToCustomDestination(job); return; } QString localDir = job.localWorkingDirectory() + "/.."; QString remoteDir = QString("%1/%2").arg(m_workingDirectoryBase) .arg(idTypeToString(job.moleQueueId())); SshConnection *conn = newSshConnection(); conn->setData(QVariant::fromValue(job)); connect(conn, SIGNAL(requestComplete()), this, SLOT(finalizeJobOutputCopiedFromServer())); if (!conn->copyDirFrom(remoteDir, localDir)) { Logger::logError(tr("Could not initialize ssh resources: user= '%1'\nhost =" " '%2' port = '%3'") .arg(conn->userName()).arg(conn->hostName()) .arg(conn->portNumber()), job.moleQueueId()); job.setJobState(MoleQueue::Error); conn->deleteLater(); return; } } void QueueRemoteSsh::finalizeJobOutputCopiedFromServer() { SshConnection *conn = qobject_cast(sender()); if (!conn) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not an SshConnection!")); return; } conn->deleteLater(); Job job = conn->data().value(); if (!job.isValid()) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender does not have an associated job!")); return; } if (conn->exitCode() != 0) { Logger::logError(tr("Error while copying job output from remote server:\n" "%1@%2:%3 --> %4\nExit code (%5) %6") .arg(conn->userName()).arg(conn->hostName()) .arg(conn->portNumber()).arg(job.localWorkingDirectory()) .arg(conn->exitCode()).arg(conn->output()), job.moleQueueId()); job.setJobState(MoleQueue::Error); return; } finalizeJobCopyToCustomDestination(job); } void QueueRemoteSsh::finalizeJobCopyToCustomDestination(Job job) { // Skip to next step if needed if (job.outputDirectory().isEmpty() || job.outputDirectory() == job.localWorkingDirectory()) { finalizeJobCleanup(job); return; } // The copy function will throw errors if needed. if (!FileSystemTools::recursiveCopyDirectory(job.localWorkingDirectory(), job.outputDirectory())) { Logger::logError(tr("Cannot copy '%1' -> '%2'.") .arg(job.localWorkingDirectory(), job.outputDirectory()), job.moleQueueId()); job.setJobState(MoleQueue::Error); return; } finalizeJobCleanup(job); } void QueueRemoteSsh::finalizeJobCleanup(Job job) { if (job.cleanLocalWorkingDirectory()) cleanLocalDirectory(job); if (job.cleanRemoteFiles()) cleanRemoteDirectory(job); job.setJobState(MoleQueue::Finished); } void QueueRemoteSsh::cleanRemoteDirectory(Job job) { QString remoteDir = QDir::cleanPath( QString("%1/%2").arg(m_workingDirectoryBase) .arg(idTypeToString(job.moleQueueId()))); // Check that the remoteDir is not just "/" due to another bug. if (remoteDir.simplified() == "/") { Logger::logError(tr("Refusing to clean remote directory %1 -- an internal " "error has occurred.").arg(remoteDir), job.moleQueueId()); return; } QString command = QString ("rm -rf %1").arg(remoteDir); SshConnection *conn = newSshConnection(); conn->setData(QVariant::fromValue(job)); connect(conn, SIGNAL(requestComplete()), this, SLOT(remoteDirectoryCleaned())); if (!conn->execute(command)) { Logger::logError(tr("Could not initialize ssh resources: user= '%1'\nhost =" " '%2' port = '%3'") .arg(conn->userName()).arg(conn->hostName()) .arg(conn->portNumber()), job.moleQueueId()); conn->deleteLater(); return; } } void QueueRemoteSsh::remoteDirectoryCleaned() { SshConnection *conn = qobject_cast(sender()); if (!conn) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not an SshConnection!")); return; } conn->deleteLater(); Job job = conn->data().value(); if (!job.isValid()) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender does not have an associated job!")); return; } if (conn->exitCode() != 0) { Logger::logError(tr("Error clearing remote directory '%1@%2:%3/%4'.\n" "Exit code (%5) %6") .arg(conn->userName()).arg(conn->hostName()) .arg(m_workingDirectoryBase) .arg(idTypeToString(job.moleQueueId())) .arg(conn->exitCode()).arg(conn->output()), job.moleQueueId()); job.setJobState(MoleQueue::Error); return; } } void QueueRemoteSsh::beginKillJob(Job job) { const QString command = QString("%1 %2") .arg(m_killCommand) .arg(idTypeToString(job.queueId())); SshConnection *conn = newSshConnection(); conn->setData(QVariant::fromValue(job)); connect(conn, SIGNAL(requestComplete()), this, SLOT(endKillJob())); if (!conn->execute(command)) { Logger::logError(tr("Could not initialize ssh resources: user= '%1'\nhost =" " '%2' port = '%3'") .arg(conn->userName()).arg(conn->hostName()) .arg(conn->portNumber()), job.moleQueueId()); job.setJobState(MoleQueue::Error); conn->deleteLater(); return; } } void QueueRemoteSsh::endKillJob() { SshConnection *conn = qobject_cast(sender()); if (!conn) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not an SshConnection!")); return; } conn->deleteLater(); Job job = conn->data().value(); if (!job.isValid()) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender does not have an associated job!")); return; } if (conn->exitCode() != 0) { Logger::logWarning(tr("Error cancelling job (mqid=%1, queueid=%2) on " "%3@%4:%5 (queue=%6)\n(%7) %8") .arg(idTypeToString(job.moleQueueId())) .arg(idTypeToString(job.queueId())) .arg(conn->userName()).arg(conn->hostName()) .arg(conn->portNumber()).arg(m_name) .arg(conn->exitCode()).arg(conn->output())); return; } job.setJobState(MoleQueue::Canceled); } SshConnection *QueueRemoteSsh::newSshConnection() { SshCommand *command = SshCommandFactory::instance()->newSshCommand(); command->setSshCommand(m_sshExecutable); command->setScpCommand(m_scpExecutable); command->setHostName(m_hostName); command->setUserName(m_userName); command->setIdentityFile(m_identityFile); command->setPortNumber(m_sshPort); return command; } QString QueueRemoteSsh::generateQueueRequestCommand() { QList queueIds = m_jobs.keys(); QString queueIdString; foreach (IdType id, queueIds) { if (id != InvalidId) queueIdString += QString::number(id) + " "; } return QString ("%1 %2").arg(m_requestQueueCommand).arg(queueIdString); } } // End namespace molequeue-0.9.0/molequeue/app/queues/remotessh.h000066400000000000000000000122001323436134600217730ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef QUEUEREMOTESSH_H #define QUEUEREMOTESSH_H #include "remote.h" class QTimer; namespace MoleQueue { class QueueManager; class SshConnection; /// @brief QueueRemote subclass for interacting with a generic Remote queue /// over SSH. class QueueRemoteSsh : public QueueRemote { Q_OBJECT public: explicit QueueRemoteSsh(const QString &queueName = "AbstractRemoteSsh", QueueManager *parentManager = 0); ~QueueRemoteSsh(); bool writeJsonSettings(QJsonObject &json, bool exportOnly, bool includePrograms) const; bool readJsonSettings(const QJsonObject &json, bool importOnly, bool includePrograms); void setSshExecutable(const QString &exe) { m_sshExecutable = exe; } QString sshExecutable() const { return m_sshExecutable; } void setScpExecutable(const QString &exe) { m_scpExecutable = exe; } QString scpExectuable() const { return m_scpExecutable; } void setHostName(const QString &host) { m_hostName = host; } QString hostName() const { return m_hostName; } void setUserName(const QString &user) { m_userName = user; } QString userName() const { return m_userName; } void setIdentityFile(const QString &identity) { m_identityFile = identity; } QString identityFile() const { return m_identityFile; } void setSshPort(int port) { m_sshPort = port; } int sshPort() const { return m_sshPort; } void setSubmissionCommand(const QString &command) { m_submissionCommand = command; } QString submissionCommand() const { return m_submissionCommand; } void setKillCommand(const QString &command) { m_killCommand = command; } QString killCommand() const { return m_killCommand; } void setRequestQueueCommand(const QString &command) { m_requestQueueCommand = command; } QString requestQueueCommand() const { return m_requestQueueCommand; } virtual AbstractQueueSettingsWidget* settingsWidget(); public slots: void requestQueueUpdate(); protected slots: void createRemoteDirectory(MoleQueue::Job job); void remoteDirectoryCreated(); void copyInputFilesToHost(MoleQueue::Job job); void inputFilesCopied(); void submitJobToRemoteQueue(MoleQueue::Job job); void jobSubmittedToRemoteQueue(); void handleQueueUpdate(); void beginFinalizeJob(MoleQueue::IdType queueId); void finalizeJobCopyFromServer(MoleQueue::Job job); void finalizeJobOutputCopiedFromServer(); void finalizeJobCopyToCustomDestination(MoleQueue::Job job); void finalizeJobCleanup(MoleQueue::Job job); void cleanRemoteDirectory(MoleQueue::Job job); void remoteDirectoryCleaned(); void beginKillJob(MoleQueue::Job job); void endKillJob(); protected: /** * @return a new SshConnection, the caller assumes ownership */ virtual SshConnection *newSshConnection(); /** * Extract the job id from the submission output. Reimplement this in derived * classes. * @param submissionOutput Output from m_submissionCommand * @param queueId The queuing system's job id. * @return True if parsing successful, false otherwise. */ virtual bool parseQueueId(const QString &submissionOutput, IdType *queueId) = 0; /** * Prepare the command to check the remote queue. The default implementation * is m_requestQueueCommand followed by the owned job ids separated by * spaces. */ virtual QString generateQueueRequestCommand(); /** * Extract the queueId and JobState from a single line of the the queue list * output. Reimplement this in derived classes. * @param queueListOutput Single line of output from m_requestQueueCommand * @param queueId The queuing system's job id. * @param state The state of the job with id queueId * @return True if parsing successful, false otherwise. */ virtual bool parseQueueLine(const QString &queueListOutput, IdType *queueId, MoleQueue::JobState *state) = 0; QString m_sshExecutable; QString m_scpExecutable; QString m_hostName; QString m_userName; QString m_identityFile; int m_sshPort; bool m_isCheckingQueue; QString m_submissionCommand; QString m_killCommand; QString m_requestQueueCommand; /// List of allowed exit codes for m_requestQueueCommand. This is required for /// e.g. PBS/Torque, which return 153 if you request the status of a job that /// has completed. QList m_allowedQueueRequestExitCodes; }; } // End namespace #endif // QUEUEREMOTESSH_H molequeue-0.9.0/molequeue/app/queues/sge.cpp000066400000000000000000000103131323436134600210760ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "sge.h" #include "logger.h" #include namespace MoleQueue { QueueSge::QueueSge(QueueManager *parentManager) : QueueRemoteSsh("Remote (SGE)", parentManager) { m_submissionCommand = "qsub"; m_killCommand = "qdel"; m_requestQueueCommand = "qstat"; m_launchScriptName = "job.sge"; m_launchTemplate = "#!/bin/sh\n" "#\n" "# Sample job script provided by MoleQueue.\n" "#\n" "#$ -S /bin/bash\n" "#$ -N MoleQueueJob-$$moleQueueId$$\n" "##$ -pe NODETYPE $$numberOfCores$$\n" "#$ -l h_rt=$$maxWallTime$$\n" "\n" "cd $SGE_O_WORKDIR\n" "$$programExecution$$\n"; } QueueSge::~QueueSge() { } bool QueueSge::parseQueueId(const QString &submissionOutput, IdType *queueId) { // Assuming submissionOutput is: // your job ('batchFileName') has been submitted QRegExp parser ("^[Yy]our job (\\d+)"); int ind = parser.indexIn(submissionOutput); if (ind >= 0) { bool ok; *queueId = static_cast(parser.cap(1).toInt(&ok)); return ok; } return false; } QString QueueSge::generateQueueRequestCommand() { return QString ("%1 -u %2").arg(m_requestQueueCommand).arg(m_userName); } bool QueueSge::parseQueueLine(const QString &queueListOutput, IdType *queueId, JobState *state) { // Expecting qstat output is: // // job-ID prior name user state submit/start at queue function // 231 0 hydra craig r 07/13/96 durin.q MASTER // 20:27:15 // 232 0 compile penny r 07/13/96 durin.q MASTER // 20:30:40 // 230 0 blackhole don r 07/13/96 dwain.q MASTER // 20:26:10 // 233 0 mac elaine r 07/13/96 dwain.q MASTER // 20:30:40 // 234 0 golf shannon r 07/13/96 dwain.q MASTER // 20:31:44 // 236 5 word elaine qw 07/13/96 // 20:32:07 // 235 0 andrun penny qw 07/13/96 20:31:43 QRegExp parser ("^\\s*(\\d+)" // job-ID "\\s+\\S+" // prior "\\s+\\S+" // name "\\s+\\S+" // user "\\s+(\\w+)"); // state QString stateStr; int ind = parser.indexIn(queueListOutput); if (ind >= 0) { bool ok; *queueId = static_cast(parser.cap(1).toInt(&ok)); if (!ok) return false; stateStr = parser.cap(2).toLower(); if (stateStr == "r" || stateStr == "d" || // mark deleted/errored jobs as running for now stateStr == "e") { *state = MoleQueue::RunningRemote; return true; } else if (stateStr == "qw"|| stateStr == "q" || stateStr == "w" || stateStr == "s" || stateStr == "h" || stateStr == "t") { *state = MoleQueue::QueuedRemote; return true; } else { Logger::logWarning(tr("Unrecognized queue state '%1' in %2 queue '%3'. " "Queue line:\n%4") .arg(stateStr).arg(typeName()).arg(name()) .arg(queueListOutput)); return false; } } return false; } } // End namespace molequeue-0.9.0/molequeue/app/queues/sge.h000066400000000000000000000025131323436134600205460ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef QUEUESGE_H #define QUEUESGE_H #include "remotessh.h" class QueueSgeTest; namespace MoleQueue { /// @brief QueueRemote subclass for interacting with Sun Grid Engine. class QueueSge : public QueueRemoteSsh { Q_OBJECT public: explicit QueueSge(QueueManager *parentManager = 0); ~QueueSge(); QString typeName() const { return "Sun Grid Engine"; } friend class ::QueueSgeTest; protected: virtual bool parseQueueId(const QString &submissionOutput, IdType *queueId); virtual QString generateQueueRequestCommand(); virtual bool parseQueueLine(const QString &queueListOutput, IdType *queueId, JobState *state); }; } // End namespace #endif // QueueSGE_H molequeue-0.9.0/molequeue/app/queues/slurm.cpp000066400000000000000000000117741323436134600214760ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2013 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "slurm.h" #include "logger.h" #include #include namespace MoleQueue { QueueSlurm::QueueSlurm(QueueManager *parentManager) : QueueRemoteSsh("Remote (SLURM)", parentManager) { m_submissionCommand = "sbatch"; m_killCommand = "scancel"; m_requestQueueCommand = "squeue"; m_launchScriptName = "job.slurm"; m_launchTemplate = "#!/bin/sh\n" "#\n" "# Sample SLURM job script provided by MoleQueue.\n" "#\n" "# These commands set up your job:\n" "#SBATCH --job-name=\"MoleQueueJob-$$moleQueueId$$\"\n" "#SBATCH --time=$$maxWallTime$$\n" "#SBATCH --nodes=1\n" "#SBATCH --ntasks-per-node=$$numberOfCores$$\n" "\n" "cd $SLURM_SUBMIT_DIR\n" "$$programExecution$$\n"; } QueueSlurm::~QueueSlurm() { } QString QueueSlurm::generateQueueRequestCommand() { QList queueIds(m_jobs.keys()); QStringList queueIdStringList; foreach (IdType id, queueIds) { if (id != InvalidId) queueIdStringList << QString::number(id); } return QString("%1 -j %2").arg(m_requestQueueCommand) .arg(queueIdStringList.join(",")); } bool QueueSlurm::parseQueueId(const QString &submissionOutput, IdType *queueId) { // Assuming submissionOutput is: // Submitted batch job QRegExp parser("^Submitted batch job (\\d+)"); int ind = parser.indexIn(submissionOutput); if (ind >= 0) { bool ok; *queueId = static_cast(parser.cap(1).toInt(&ok)); return ok; } return false; } bool QueueSlurm::parseQueueLine(const QString &queueListOutput, IdType *queueId, JobState *state) { *queueId = InvalidId; *state = Unknown; // Expecting qstat output is: // JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON) // 4832 general-c hello_te cdc R 0:14 2 f16n[10-11] QRegExp parser("^\\s*(\\d+)" // job-ID "\\s+\\S+" // partition "\\s+\\S+" // name "\\s+\\S+" // user "\\s+(\\w+)"); // state int ind = parser.indexIn(queueListOutput); if (ind >= 0) { bool ok; *queueId = static_cast(parser.cap(1).toInt(&ok)); if (!ok) return false; QString stateStr(parser.cap(2).toLower()); // JOB STATE CODES // // Jobs typically pass through several states in the course of // their execution. The typical states are PENDING, RUNNING, // SUSPENDED, COMPLETING, and COMPLETED. An explanation of each // state follows. // // CA CANCELLED Job was explicitly cancelled by the user or system // administrator. The job may or may not have been // initiated. // CD COMPLETED Job has terminated all processes on all nodes. // CF CONFIGURING Job has been allocated resources, but are waiting // for them to become ready for use (e.g. booting). // CG COMPLETING Job is in the process of completing. Some processes // on some nodes may still be active. // F FAILED Job terminated with non-zero exit code or other // failure condition. // NF NODE_FAIL Job terminated due to failure of one or more allo- // cated nodes. // PD PENDING Job is awaiting resource allocation. // R RUNNING Job currently has an allocation. // S SUSPENDED Job has an allocation, but execution has been sus- // pended. // TO TIMEOUT Job terminated upon reaching its time limit. if (stateStr == "ca" || stateStr == "cd" || stateStr == "cg" || stateStr == "f" || stateStr == "nf" || stateStr == "pr" || stateStr == "r" || stateStr == "s" || stateStr == "to") { *state = MoleQueue::RunningRemote; return true; } else if (stateStr == "cf" || stateStr == "pd") { *state = MoleQueue::QueuedRemote; return true; } else { Logger::logWarning(tr("Unrecognized queue state '%1' in %2 queue '%3'. " "Queue line:\n'%4'") .arg(stateStr).arg(typeName()).arg(name()) .arg(queueListOutput)); return false; } } return false; } } // End namespace molequeue-0.9.0/molequeue/app/queues/slurm.h000066400000000000000000000024741323436134600211400ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2013 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef QUEUESLURM_H #define QUEUESLURM_H #include "remotessh.h" class QueueSlurmTest; namespace MoleQueue { /// @brief QueueRemote subclass for interacting with a SLURM managed queue. class QueueSlurm : public QueueRemoteSsh { Q_OBJECT public: explicit QueueSlurm(QueueManager *parentManager = 0); ~QueueSlurm(); QString typeName() const { return "SLURM"; } friend class ::QueueSlurmTest; protected: QString generateQueueRequestCommand(); bool parseQueueId(const QString &submissionOutput, IdType *queueId); bool parseQueueLine(const QString &queueListOutput, IdType *queueId, JobState *state); }; } // End namespace MoleQueue #endif // QUEUESLURM_H molequeue-0.9.0/molequeue/app/queues/uit/000077500000000000000000000000001323436134600204175ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/queues/uit/authenticatecont.cpp000066400000000000000000000031601323436134600244650ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "authenticatecont.h" #include namespace MoleQueue { namespace Uit { AuthenticateCont::AuthenticateCont(const QString authSessionId, const QList prompts) : m_authSessionId(authSessionId), m_prompts(prompts) { } QString AuthenticateCont::toXml() const { QString xml; QXmlStreamWriter xmlWriter(&xml); xmlWriter.writeStartDocument(); xmlWriter.writeStartElement("AuthenticateCont"); xmlWriter.writeTextElement("auth__session__id", m_authSessionId); xmlWriter.writeStartElement("prompts"); foreach(const Prompt &p, m_prompts) { xmlWriter.writeStartElement("Prompt"); xmlWriter.writeTextElement("id", QString::number(p.id())); xmlWriter.writeTextElement("prompt", p.prompt()); xmlWriter.writeTextElement("reply", p.userResponse()); xmlWriter.writeEndElement(); } xmlWriter.writeEndElement(); xmlWriter.writeEndElement(); return xml; } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/authenticatecont.h000066400000000000000000000025641323436134600241410ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef AUTHENTICATECONT_H_ #define AUTHENTICATECONT_H_ #include "authenticateresponse.h" namespace MoleQueue { namespace Uit { /// @brief class using to model UIT AuthenticateCont message. class AuthenticateCont { public: /** * @param authSessionId The current UIT auth session ID * @param prompts The prompts that the server has requested, including the * user responses, */ AuthenticateCont(const QString authSessionId, const QList prompts); /** * @return the XML representation of this instance to send to the UIT server. */ QString toXml() const; private: QString m_authSessionId; QList m_prompts; }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* AUTHENTICATECONT_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/authenticateresponse.cpp000066400000000000000000000112221323436134600253560ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "authenticateresponse.h" #include "messagehandler.h" #include "logger.h" #include namespace MoleQueue { namespace Uit { Prompt::Prompt(int i, const QString p) : m_id(i), m_prompt(p) { } Prompt::Prompt(const Prompt &p) : m_id(p.id()), m_prompt(p.prompt()), m_userResponse(p.userResponse()) { } AuthenticateResponse::AuthenticateResponse() : m_hasPrompts(false), m_success(false), m_valid(false) { } AuthenticateResponse::AuthenticateResponse(const AuthenticateResponse &response) : m_authSessionId(response.authSessionId()), m_hasPrompts(response.hasPrompts()), m_prompts(response.prompts()), m_success(response.success()), m_errorMessage(response.errorMessage()), m_banner(response.banner()),m_token(response.token()), m_valid(response.isValid()) { } AuthenticateResponse &AuthenticateResponse::operator=( const AuthenticateResponse &other) { if (this != &other) { m_authSessionId = other.authSessionId(); m_hasPrompts = other.hasPrompts(); m_prompts = other.prompts(); m_success = other.success(); m_errorMessage = other.errorMessage(); m_banner = other.banner(); m_token = other.token(); m_valid = other.isValid(); } return *this; } void AuthenticateResponse::setContent(const QString &xml) { m_valid = true; MessageHandler handler; QXmlQuery query; query.setMessageHandler(&handler); m_valid = query.setFocus(xml); if (!m_valid) return; // Get the session id QString authId; query.setQuery("/AuthenticateResponse/auth__session__id/string()"); m_valid = query.evaluateTo(&authId); if (!m_valid) return; m_authSessionId = authId.trimmed(); // Was the call successful QString successful; query.setQuery("/AuthenticateResponse/success/string()"); m_valid = query.evaluateTo(&successful); if (!m_valid) return; m_success = successful.trimmed().toLower() == "true"; // Do we have prompts QString hasProm; query.setQuery("/AuthenticateResponse/has__prompts/string()"); m_valid = query.evaluateTo(&hasProm); if (!m_valid) return; m_hasPrompts = hasProm.trimmed().toLower() == "true"; // Get the banner QString ban; query.setQuery("/AuthenticateResponse/banner/string()"); m_valid = query.evaluateTo(&ban); if (!m_valid) return; m_banner = ban.trimmed(); // Get the token, if there is one QString tok; query.setQuery("/AuthenticateResponse/token/string()"); m_valid = query.evaluateTo(&tok); if (!m_valid) return; m_token = tok.trimmed(); // if we have prompts then get them if (m_hasPrompts) { query.setQuery("/AuthenticateResponse/prompts/Prompt/id/string()"); QStringList ids; m_valid = query.evaluateTo(&ids); if (!m_valid) return; foreach (const QString &id, ids) { query.bindVariable("id", QVariant(id)); query.setQuery(QString("/AuthenticateResponse/prompts/Prompt[id=$id]/" \ "prompt/string()")); QString prompt; m_valid = query.evaluateTo(&prompt); if (!m_valid) return; m_prompts.append(Prompt(id.toInt(), prompt.trimmed())); } } QString error; query.setQuery("/AuthenticateResponse/error__message/string()"); m_valid = query.evaluateTo(&error); m_errorMessage = error.trimmed(); } QString AuthenticateResponse::authSessionId() const { return m_authSessionId; } bool AuthenticateResponse::hasPrompts() const { return m_hasPrompts; } QList AuthenticateResponse::prompts() const { return m_prompts; } bool AuthenticateResponse::success() const { return m_success; } QString AuthenticateResponse::errorMessage() const { return m_errorMessage; } QString AuthenticateResponse::banner() const { return m_banner; } QString AuthenticateResponse::token() const { return m_token; } bool AuthenticateResponse::isValid() const { return m_valid; } AuthenticateResponse AuthenticateResponse::fromXml(const QString &xml) { AuthenticateResponse response; response.setContent(xml); return response; } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/authenticateresponse.h000066400000000000000000000067211323436134600250330ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef AUTHENTICATERESPONSE_H_ #define AUTHENTICATERESPONSE_H_ #include #include namespace MoleQueue { namespace Uit { /** * @brief class used to model a UIT prompt. */ class Prompt { public: /** * @param id The prompt is provided by UIT. * @param prompt The prompt string for example "Password". */ Prompt(int id, const QString prompt); Prompt(const Prompt &p); /** * @return The prompt ID */ int id() const { return m_id; } /** * @return The prompt to display to the user for example "Password". */ QString prompt() const { return m_prompt; } /** * @param Set the value the user has entered for this prompt. */ void setUserResponse(const QString &response) { m_userResponse = response; } /** * @return The value the user entered for this prompt. */ QString userResponse() const { return m_userResponse; } private: int m_id; QString m_prompt; QString m_userResponse; }; /** * @brief class used to model UIT AuthenticateResponse */ class AuthenticateResponse { public: AuthenticateResponse(); AuthenticateResponse(const AuthenticateResponse &response); AuthenticateResponse &operator=(const AuthenticateResponse &other); /** * @return The current UIT auth session id */ QString authSessionId() const; /** * @return True is the underlying UIT message has user prompts, false * otherwise */ bool hasPrompts() const; /** * @return The list of prompts that need to presented to the user. */ QList prompts() const; /** * @return true, if the authenticateUser() call was successful, false * otherwise. */ bool success() const; /** * @return Any error message associated with this response. */ QString errorMessage() const; /** * @return Banner text to be displayed to the user along with the prompts. */ QString banner() const; /** * @return The session token returned when authentication was successful. */ QString token() const; /** * @return true is the XML message provided by the server was valid, false * otherwise. */ bool isValid() const; /** * Static method to create a AuthenticateResponse instance from a incoming * XML message. * * @param The XML message. */ static AuthenticateResponse fromXml(const QString &xml); private: QString m_authSessionId; bool m_hasPrompts; QList m_prompts; bool m_success; QString m_errorMessage; QString m_banner; QString m_token; bool m_valid; /** * Set the XML associated with a AuthenticateResponse instance, this method * uses XPath the pull out the pertinent parts of the message and init. the * appropriate members. */ void setContent(const QString &xml); }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* AUTHENTICATERESPONSE_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/authenticator.cpp000066400000000000000000000151031323436134600237750ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "kerberoscredentials.h" #include "authenticator.h" #include "credentialsdialog.h" #include "../queueuit.h" #include "mainwindow.h" #include "logger.h" namespace MoleQueue { namespace Uit { Authenticator::Authenticator(UitapiService *uit, const QString &kerberosPrinciple, QObject *parentObject, QWidget *dialogParent) : QObject(parentObject), m_uit(uit), m_dialogParent(dialogParent), m_kerberosPrinciple(kerberosPrinciple), m_credentialsDialog(NULL) { connect(m_uit, SIGNAL(authenticateUserError(const KDSoapMessage&)), this, SLOT(authenticateUserError(const KDSoapMessage&))); } Authenticator::~Authenticator() { } void Authenticator::authenticate() { showCredentialsDialog(tr("Enter Kerberos credentials for '%1'") .arg(m_kerberosPrinciple), "Password", SLOT(authenticateKerberosCredentials(const QString&))); } void Authenticator::authenticateKerberosCredentials(const QString &password) { Uit::KerberosCredentials credentials(m_kerberosPrinciple, password); m_credentialsDialog->disconnect(SIGNAL(entered(const QString&))); // disconnect to make sure we don't connect twice, is there a better way? m_uit->disconnect(SIGNAL(authenticateUserDone(const QString&))); connect(m_uit, SIGNAL(authenticateUserDone(const QString&)), this, SLOT(authenticateKerberosResponse(const QString&))); m_uit->asyncAuthenticateUser(credentials.toXml(), ::MoleQueue::QueueUit::clientId); } void Authenticator::authenticateKerberosResponse(const QString &responseXml) { disconnect(m_uit, SIGNAL(authenticateUserDone(const QString&)), this, SLOT(authenticateKerberosResponse(const QString&))); AuthenticateResponse response = AuthenticateResponse::fromXml(responseXml); if (!response.isValid()) { QString errorMessage("Server return an invalid authenticate response to " \ "kerberos credentials"); Logger::logError(errorMessage); emit authenticationError(errorMessage); return; } // If error message was return display and prompt user again. if (response.errorMessage().length() == 0) { m_credentialsDialog->setErrorMessage(response.errorMessage()); m_credentialsDialog->show(); } else { m_credentialsDialog->close(); // disconnect to make sure we don't connect twice, is there a better way? m_uit->disconnect(SIGNAL(authenticateUserDone(const QString&))); // reconnect authenticateUserDone signal and move to next step connect(m_uit, SIGNAL(authenticateUserDone(const QString&)), this, SLOT(authenticateResponse(const QString&))); authenticateResponse(response); } } void Authenticator::authenticateResponse(const AuthenticateResponse &response) { m_authSessionId = response.authSessionId(); if (response.hasPrompts()) { // Walk through each prompts getting the credentials from the user. AuthResponseProcessor *processor = new AuthResponseProcessor(response, m_credentialsDialog); connect(processor, SIGNAL(complete(const AuthenticateCont&)), this, SLOT(authenticateCont(const AuthenticateCont&))); processor->process(); } else { // If the call was successfully and there are no more prompts // then we are authenticated if (response.success()) { emit authenticationComplete(response.token()); // We are done ... return; } else { // The server has provided a reason for the failure, display to the user // and start the process again. if (response.errorMessage().length() != 0) { m_credentialsDialog->setErrorMessage(response.errorMessage()); showKerberosCredentialsDialog(); } else { QString errorMessage("An error occurred authenticating, server " \ "provided no error message."); Logger::logError(errorMessage); emit authenticationError(errorMessage); } } } } void Authenticator::authenticateResponse(const QString &responseXml) { AuthenticateResponse response = AuthenticateResponse::fromXml(responseXml); if (!response.isValid()) { QString errorMessage = tr("Server return an invalid authenticate response"); Logger::logError(errorMessage); emit authenticationError(errorMessage); return; } authenticateResponse(response); } void Authenticator::authenticateCont(const AuthenticateCont &authCont) { AuthResponseProcessor *processor = qobject_cast(sender()); if (processor) { QString response = authCont.toXml(); m_uit->asyncAuthenticateUser(response, ::MoleQueue::QueueUit::clientId); processor->deleteLater(); } else { Logger::logError("Unable to get PromptProcessor"); } } void Authenticator::authenticateUserError(const KDSoapMessage& fault) { emit authenticationError(fault.faultAsString()); } void Authenticator::showKerberosCredentialsDialog() { showCredentialsDialog(tr("Enter Kerberos credentials for '%1'") .arg(m_kerberosPrinciple), "Password", SLOT(authenticateKerberosCredentials(const QString&))); } void Authenticator::showCredentialsDialog(const QString banner, const QString prompt, const char *enteredSlot) { if (m_credentialsDialog == NULL) m_credentialsDialog = new CredentialsDialog(m_dialogParent); m_credentialsDialog->setPrompt(prompt); m_credentialsDialog->setHostString(banner); m_credentialsDialog->disconnect(); connect(m_credentialsDialog, SIGNAL(entered(const QString&)), this, enteredSlot); connect(m_credentialsDialog, SIGNAL(canceled()), this, SIGNAL(authenticationCanceled())); m_credentialsDialog->show(); } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/authenticator.h000066400000000000000000000076161323436134600234540ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef UITAUTHENTICATOR_H_ #define UITAUTHENTICATOR_H_ #include "authenticateresponse.h" #include "authenticatecont.h" #include "wsdl_uitapi.h" #include namespace MoleQueue { class CredentialsDialog; namespace Uit { /** * @brief class used to perform UIT authentication steps. */ class Authenticator : public QObject { Q_OBJECT public: Authenticator(UitapiService *uit, const QString &kerberosPrinciple, QObject *parentObject = 0, QWidget *dialogParent = 0); ~Authenticator(); signals: /** * Emitted when the authentication is successfully completed. * * @param token The user token for the session */ void authenticationComplete(const QString &token); /** * Emitted if an error occurs during authentication. */ void authenticationError(const QString &errorMessage); /** * Emitted if an user cancels authentication. */ void authenticationCanceled(); public slots: /** * Start the process of authenticating with the UIT server. */ void authenticate(); private: UitapiService *m_uit; QWidget *m_dialogParent; QString m_authSessionId; QString m_kerberosPrinciple; /// Dialog uses to enter credentials CredentialsDialog *m_credentialsDialog; /** * display the credentials dialog used to enter kerberos credentials */ void showKerberosCredentialsDialog(); /** * * @param banner The text to display to the user. * @param prompt The prompt to display to the user. * @param enteredSlot The slot to call when the user has entered a response * to the prompt. */ void showCredentialsDialog(const QString banner, const QString prompt, const char *enteredSlot); private slots: /** * Send Kerberos credentials to the UIT server. authenticateKerberosResponse * will be called with the servers response. * * @param password The users Kerberos password. */ void authenticateKerberosCredentials(const QString &password); /** * Called with the servers response to Kerberos authentication message. * * @param responseXml The XML containing the servers response. */ void authenticateKerberosResponse(const QString &responseXml); /** * Called with the servers response to an authenticateUser(...) call. * Constructs an AuthenticateResponse from the XML and delegates to the * overloaded version. * * @param responseXml The XML from the server. * */ void authenticateResponse(const QString &responseXml); /** * Process a AuthenticateResponse message. Walk through prompts requesting * user responses. * * @param reponse The reponse instance. */ void authenticateResponse(const AuthenticateResponse &response); /** * Called by the AuthResponseProcessor. Provide the appropriate * AuthenticateCont message containing the user responses that can be sent * back to the UIT server */ void authenticateCont(const AuthenticateCont &authenticateCont); /** * Called is an error occurs during the execution of a authenticateUser(...) * call. * * @fault The KDSoap object contains details of the error. */ void authenticateUserError(const KDSoapMessage &fault); }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* UITAUTHENTICATOR_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/authresponseprocessor.cpp000066400000000000000000000041761323436134600256130ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "authresponseprocessor.h" #include "credentialsdialog.h" namespace MoleQueue { namespace Uit { AuthResponseProcessor::AuthResponseProcessor( const AuthenticateResponse &response, CredentialsDialog *credentialsDialog, QObject *parentObject) : QObject(parentObject), m_authenticateResponse(response), m_currentIndex(0), m_credentialsDialog(credentialsDialog) { m_prompts = response.prompts(); connect(m_credentialsDialog, SIGNAL(entered(const QString&)), this, SLOT(processCredentials(const QString&))); } void AuthResponseProcessor::process() { m_credentialsDialog->setHostString(m_authenticateResponse.banner()); nextPrompt(); } void AuthResponseProcessor::nextPrompt() { // We still have prompts to present to the user. if (m_currentIndex < m_prompts.size()) { Prompt p = m_prompts[m_currentIndex]; m_credentialsDialog->setPrompt(p.prompt()); m_credentialsDialog->show(); m_credentialsDialog->raise(); m_credentialsDialog->activateWindow(); } // We are done so can contruct a AuthenticateCont message else { m_credentialsDialog->close(); m_credentialsDialog->disconnect(SIGNAL(entered(const QString&))); AuthenticateCont authCont(m_authenticateResponse.authSessionId(), m_prompts); emit complete(authCont); } } void AuthResponseProcessor::processCredentials(const QString &credentials) { m_prompts[m_currentIndex++].setUserResponse(credentials); nextPrompt(); } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/authresponseprocessor.h000066400000000000000000000045731323436134600252610ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef AUTHRESPONSEPROCESSOR_H_ #define AUTHRESPONSEPROCESSOR_H_ #include "authenticateresponse.h" #include "authenticatecont.h" #include #include namespace MoleQueue { class CredentialsDialog; namespace Uit { /** * @brief Class used to process a AuthenticateResponse message. Basically walks * through the list of prompts provided by the server asking the user for the * appropriate responses. */ class AuthResponseProcessor : public QObject { Q_OBJECT public: /** * @param response The reponse object * @param credentialsDialog The dialog that is presented to the user to * request responses to each prompt. The class does not assume responsibility * for the object. */ AuthResponseProcessor(const AuthenticateResponse &response, CredentialsDialog *credentialsDialog, QObject *parentObject = 0); /** * Start process the response */ void process(); signals: /** * Emitted when all the user responses to the prompts have be collected. * * @param authenticateCont The AuthenticateCont message that can be sent to * the UIT server containing the user responses. */ void complete(const AuthenticateCont &authenticateCont); private slots: /** * Process the next prompt in the list. */ void nextPrompt(); /** * Set the the user response for the prompt currently being processed */ void processCredentials(const QString &credentials); private: AuthenticateResponse m_authenticateResponse; int m_currentIndex; CredentialsDialog *m_credentialsDialog; /// list contain prompts with user responses filled out. QList m_prompts; }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* AUTHRESPONSEPROCESSOR_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/compositeiodevice.cpp000066400000000000000000000041601323436134600246360ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "compositeiodevice.h" #include "logger.h" namespace MoleQueue { namespace Uit { CompositeIODevice::CompositeIODevice(QObject *parentObject) : QIODevice(parentObject), m_deviceIndex(0) { } bool CompositeIODevice::addDevice(QIODevice *device) { bool added = false; // The device should be readable. if (device->isReadable()) { m_devices.append(device); added = true; } return added; } qint64 CompositeIODevice::readData(char* data, qint64 maxSize) { // We have no more devices to read so we are done. if (m_deviceIndex >= m_devices.size()) return -1; QIODevice *device = m_devices[m_deviceIndex]; qint64 bytesRead = device->read(data, maxSize); while (bytesRead < maxSize) { // If the current device is done move on the next in the list. if (device->atEnd()) { m_deviceIndex++; if (m_deviceIndex < m_devices.size()) device = m_devices[m_deviceIndex]; else // No more devices to read break; } qint64 leftToRead = maxSize - bytesRead; bytesRead += device->read(data + bytesRead, leftToRead); } return bytesRead; } qint64 CompositeIODevice::writeData(const char * data, qint64 maxSize) { Q_UNUSED(data); Q_UNUSED(maxSize); Logger::logError("writeData not supported"); return -1; } qint64 CompositeIODevice::size () const { qint64 totalSize = 0; foreach(const QIODevice *device, m_devices) totalSize += device->size(); return totalSize; } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/compositeiodevice.h000066400000000000000000000035601323436134600243060ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef COMPOSITEIODEVICE_H_ #define COMPOSITEIODEVICE_H_ #include #include namespace MoleQueue { namespace Uit { /** * @class CompositeIODevice compositeiodevice.h * * @brief The CompositeIODevice class is facade that allows several QIODevices * into a single QIODevice. * */ class CompositeIODevice: public QIODevice { Q_OBJECT public: CompositeIODevice(QObject *parentObject = 0); /** * Add a QIODevice to the device. The QIODevice being added must be open in * read mode. * * @param device The QIODevice to add. */ bool addDevice(QIODevice *device); /** * @return The combine size of all the QIODevices this composite represents. */ qint64 size () const; protected: /** * Override superclass with composite read. */ qint64 readData(char* data, qint64 maxSize); /** * Override superclass, write is not supported. */ qint64 writeData ( const char * data, qint64 maxSize ); private: /// The list of QIODevices in the composite. QList m_devices; /// The index of the QIODevice currently being read. int m_deviceIndex; }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* COMPOSITEIODEVICE_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/directorycreate.cpp000066400000000000000000000065151323436134600243220ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "directorycreate.h" #include "requests.h" #include "filestreamingdata.h" #include "logger.h" #include "session.h" #include #include namespace MoleQueue { namespace Uit { DirectoryCreate::DirectoryCreate(Session *session, QObject *parentObject) : FileSystemOperation(session, parentObject) { } void DirectoryCreate::start() { if (m_directory.isEmpty()) { Logger::logWarning("Trying to create empty directory!"); return; } m_parts = m_directory.split("/"); m_parts.removeAll(""); if (m_directory[0] == '/') m_currentDirectory = "/"; createNext(); } void DirectoryCreate::createNext() { if (m_parts.isEmpty()) { emit finished(); return; } if (!m_currentDirectory.isEmpty() && m_currentDirectory[m_currentDirectory.length()-1] != '/') m_currentDirectory += "/"; m_currentDirectory += m_parts.takeFirst(); StatFileRequest *statRequest = new StatFileRequest(m_session, this); statRequest->setHostId(m_hostID); statRequest->setUserName(m_userName); statRequest->setFilename(m_currentDirectory); connect(statRequest, SIGNAL(finished()), this, SLOT(processStatResponse())); connect(statRequest, SIGNAL(error(const QString &)), this, SLOT(statError(const QString &))); statRequest->submit(); } void DirectoryCreate::processStatResponse() { StatFileRequest *request = qobject_cast(sender()); if (!request) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not StatFileRequest!")); return; } request->deleteLater(); createNext(); } void DirectoryCreate::statError(const QString &errorString) { if (errorString.contains(FileSystemOperation::noSuchFileOrDir)) createDirectory(m_currentDirectory); else requestError(errorString); } void DirectoryCreate::createDirectory(const QString &dir) { CreateDirectoryRequest *request = new CreateDirectoryRequest(m_session, this); request->setHostId(m_hostID); request->setUserName(m_userName); request->setDirectory(dir); connect(request, SIGNAL(finished()), this, SLOT(createDirectoryComplete())); connect(request, SIGNAL(error(const QString &)), this, SLOT(requestError(const QString &))); request->submit(); } void DirectoryCreate::createDirectoryComplete() { CreateDirectoryRequest *request = qobject_cast(sender()); if (!request) { Logger::logError(tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not CreateDirectoryRequest!")); return; } request->deleteLater(); createNext(); } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/directorycreate.h000066400000000000000000000037571323436134600237740ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef UITDIRECTORYCREATE_H_ #define UITDIRECTORYCREATE_H_ #include "filesystemoperation.h" #include #include #include #include namespace MoleQueue { namespace Uit { class Session; /** * @brief File system operation to create a directory on a UIT host. */ class DirectoryCreate: public FileSystemOperation { Q_OBJECT public: /** * @param session The UIT session. * @param parentObject The parent object. */ DirectoryCreate(Session *session, QObject *parentObject = 0); void start(); /** * @return The directory to create. */ QString directory() const { return m_directory; } /** * @param dir The directory to create. */ void setDirectory(const QString& dir) { m_directory = dir; } private slots: /** * Process the next part of the path. */ void createNext(); /** * Slot called when the current operation is complete. */ void createDirectoryComplete(); void processStatResponse(); void statError(const QString &errorString); private: QString m_directory; // The individual parts of the directory. QStringList m_parts; // The current directory being created. QString m_currentDirectory; void createDirectory(const QString &dir); }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* UITDIRECTORYCREATE_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/directorydelete.cpp000066400000000000000000000076761323436134600243320ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "directorydelete.h" #include "requests.h" #include "logger.h" #include "session.h" #include "filestreamingdata.h" #include #include namespace MoleQueue { namespace Uit { DirectoryDelete::DirectoryDelete(Session *session, QObject *parentObject) : FileSystemOperation(session, parentObject) { } void DirectoryDelete::start() { m_dirsToProcess.push(m_directory); deleteNext(); } void DirectoryDelete::processDirectoryListing() { GetDirectoryListingRequest *request = qobject_cast(sender()); if (!request) { QString msg = tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not GetDirectoryListingRequest!"); Logger::logError(msg); emit error(msg); return; } request->deleteLater(); DirListingInfo info = request->dirListingInfo(); m_processedDirs.push(info.currentDirectory()); // Process files foreach(const FileInfo& file, info.files()) { // enqueue file include full path ... m_files.append(info.currentDirectory() + "/" + file.name()); } // Process directories foreach(const FileInfo& dir, info.directories()) { // enqueue directory include full path ... if (dir.name() != "." && dir.name() != "..") m_dirsToProcess.push(info.currentDirectory() + "/" + dir.name()); } deleteNext(); } void DirectoryDelete::deleteNext() { Request *request = qobject_cast(sender()); if (request) request->deleteLater(); // Delete all file in a directory first if (!m_files.isEmpty()) { QString remoteFilePath = m_files.takeFirst(); DeleteFileRequest *deleteRequest = new DeleteFileRequest(m_session, this); deleteRequest->setHostId(m_hostID); deleteRequest->setUserName(m_userName); deleteRequest->setFile(remoteFilePath); connect(request, SIGNAL(finished()), this, SLOT(deleteNext())); connect(request, SIGNAL(error(const QString &)), this, SLOT(requestError(const QString &))); deleteRequest->submit(); } // Do we have more directories to explorer else if (!m_dirsToProcess.isEmpty()) { QString remoteDirPath = m_dirsToProcess.pop(); GetDirectoryListingRequest *listRequest = new GetDirectoryListingRequest(m_session, this); listRequest->setDirectory(remoteDirPath); listRequest->setHostId(m_hostID); listRequest->setUserName(m_userName); connect(listRequest, SIGNAL(finished()), this, SLOT(processDirectoryListing())); connect(listRequest, SIGNAL(error(const QString &)), this, SLOT(requestError(const QString &))); listRequest->submit(); } // Finally clean up all the directories else if (!m_processedDirs.isEmpty()) { QString remoteDirPath = m_processedDirs.pop(); DeleteDirectoryRequest *deleteRequest = new DeleteDirectoryRequest(m_session, this); deleteRequest->setHostId(m_hostID); deleteRequest->setUserName(m_userName); deleteRequest->setDirectory(remoteDirPath); connect(deleteRequest, SIGNAL(finished()), this, SLOT(deleteNext())); connect(deleteRequest, SIGNAL(error(const QString &)), this, SLOT(requestError(const QString &))); deleteRequest->submit(); } else { emit finished(); } } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/directorydelete.h000066400000000000000000000040111323436134600237530ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef UITDIRDELETER_H_ #define UITDIRDELETER_H_ #include "filesystemoperation.h" #include #include #include #include namespace MoleQueue { namespace Uit { class Session; /** * @brief File system operation to delete a directory on a remote UIT system. */ class DirectoryDelete : public FileSystemOperation { Q_OBJECT public: /** * @param session The UIT session. * @param parentObject The parent object. */ DirectoryDelete(Session *session, QObject *parentObject); /** * @return The directory being deleted. */ QString directory() const { return m_directory; } /** * @param dir The directory to delete. */ void setDirectory(const QString& dir) { m_directory = dir; } void start(); private slots: /** * Slot to perform next delete operation. */ void deleteNext(); /** * Slot called to process the directory listing request from UIT. */ void processDirectoryListing(); private: QString m_directory; // File currently being deleted. QList m_files; // Directories that still need to be processed. QStack m_dirsToProcess; // Directories that have been processed i.e. a directory listing request has // been made. QStack m_processedDirs; }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* UITDIRDELETER_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/directorydownload.cpp000066400000000000000000000133721323436134600246650ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "directorydownload.h" #include "requests.h" #include "logger.h" #include "session.h" #include "filestreamingdata.h" #include "filesystemoperation.h" #include #include namespace MoleQueue { namespace Uit { DirectoryDownload::DirectoryDownload(Session *session, QObject *parentObject) : FileSystemOperation(session, parentObject) { m_networkAccess = new QNetworkAccessManager(this); connect(m_networkAccess, SIGNAL(finished(QNetworkReply*)), this, SLOT(finished(QNetworkReply*))); } void DirectoryDownload::start() { GetStreamingFileDownloadURLRequest *request = new GetStreamingFileDownloadURLRequest(m_session, this); connect(request, SIGNAL(finished()), this, SLOT(downloadInternal())); connect(request, SIGNAL(error(const QString &)), this, SLOT(requestError(const QString &))); request->submit(); } void DirectoryDownload::downloadInternal() { GetStreamingFileDownloadURLRequest *request = qobject_cast(sender()); if (!request) { QString msg = tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not GetStreamingFileDownloadURLRequest!"); Logger::logError( msg); emit error(msg); return; } request->deleteLater(); m_url = request->url(); m_directories.enqueue(m_remotePath); downloadNext(); } void DirectoryDownload::download(const QString &dir) { GetDirectoryListingRequest *request = new GetDirectoryListingRequest(m_session, this); request->setDirectory(dir); request->setHostId(m_hostID); request->setUserName(m_userName); connect(request, SIGNAL(finished()), this, SLOT(processDirectoryListing())); connect(request, SIGNAL(error(const QString &)), this, SLOT(requestError(const QString &))); request->submit(); } void DirectoryDownload::processDirectoryListing() { GetDirectoryListingRequest *request = qobject_cast(sender()); if (!request) { QString msg = tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not GetDirectoryListingRequest!"); Logger::logError(msg); emit error(msg); return; } request->deleteLater(); DirListingInfo info = request->dirListingInfo(); if (!info.isValid()) { QString msg = tr("Invalid response from UIT server: %1") .arg(info.xml()); Logger::logError(msg); emit error(msg); return; } // Process directories foreach(const FileInfo& dir, info.directories()) { // enqueue directory include full path ... if (dir.name() != "." && dir.name() != "..") m_directories.enqueue(info.currentDirectory() + "/" + dir.name()); } // Process files foreach(const FileInfo& file, info.files()) { // enqueue directory include full path ... m_files.enqueue(info.currentDirectory() + "/" + file.name()); } downloadNext(); } void DirectoryDownload::downloadNext() { // Process all files in a directory first if (!m_files.isEmpty()) { QString remoteFilePath = m_files.dequeue(); QString localFilePath = remoteFilePath; localFilePath = m_localPath + localFilePath.replace(m_remotePath, ""); // Ensure the directory exists QString path = QFileInfo(localFilePath).path(); if (!QDir(m_localPath).mkpath(path)) { QString msg = tr("Unable to create directory: %1").arg(path); Logger::logError(msg); emit error(msg); return; } // Save the file path so we know where to write the data. m_currentFilePath = localFilePath; // Now transfer the file FileStreamingData fileData; fileData.setToken(m_session->token()); fileData.setFileName(remoteFilePath); fileData.setUserName(m_userName); fileData.setHostID(m_hostID); QString xml = fileData.toXml(); QByteArray bytes; QTextStream stream(&bytes); stream << xml.size(); stream << "|"; stream << xml; stream.flush(); QNetworkRequest request; request.setUrl(QUrl(m_url)); request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/xml")); m_networkAccess->post(request, bytes); } else if (!m_directories.isEmpty()) { QString nextDir = m_directories.dequeue(); download(nextDir); } else { emit FileSystemOperation::finished(); } } void DirectoryDownload::finished(QNetworkReply *reply) { if (reply->error() == QNetworkReply::NoError) { QFile file(m_currentFilePath); if (!file.open(QFile::WriteOnly)) { QString msg = tr("Unable to open file for write: %1") .arg(m_currentFilePath); Logger::logError(msg); emit error(msg); return; } qint64 bytesRead = -1; char bytes[4048]; while((bytesRead = reply->read(bytes, 4048)) != -1) { file.write(bytes, bytesRead); } file.close(); downloadNext(); } else { Logger::logError(tr("Error downloading file: %1") .arg(reply->errorString()), m_job.moleQueueId()); emit error(reply->errorString()); } } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/directorydownload.h000066400000000000000000000053021323436134600243240ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef UITDIRDOWNLOADER_H_ #define UITDIRDOWNLOADER_H_ #include "filesystemoperation.h" #include #include #include #include namespace MoleQueue { namespace Uit { class Session; /** * @brief File system operation to download a directory from a remote UIT system. */ class DirectoryDownload : public FileSystemOperation { Q_OBJECT public: /** * @param session The UIT session. * @param parentObject The parent object. */ DirectoryDownload(Session *session, QObject *parentObject); /** * @return The remote path being downloaded. */ QString remotePath() const { return m_remotePath; } /** * @param path The remote path to be downloaded. */ void setRemotePath(const QString& path) { m_remotePath = path; } /** * @return The local path to download the directory to. */ QString localPath() const { return m_localPath; } /** * @param path The local path to download the directory to. */ void setLocalPath(const QString& path) { m_localPath = path; } /** * @return The download URL. */ QString url() const { return m_url; } /** * @param The download URL to use. */ void setUrl(const QString& u) { m_url = u; } void start(); private slots: void download(const QString &dir); void downloadInternal(); void downloadNext(); /** * Slot to process a directory listing. */ void processDirectoryListing(); /** * Slot called when the current download request is complete. * * @param reply The reply used to read the file contents. */ void finished(QNetworkReply *reply); private: QString m_remotePath; QString m_localPath; QNetworkAccessManager *m_networkAccess; QString m_url; // Queue of directories to download. QQueue m_directories; // Queue of files to download. QQueue m_files; // The current local file path to write data to. QString m_currentFilePath; }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* UITDIRDOWNLOADER_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/directoryupload.cpp000066400000000000000000000123751323436134600243440ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "directoryupload.h" #include "requests.h" #include "filestreamingdata.h" #include "logger.h" #include "session.h" #include "compositeiodevice.h" #include #include namespace MoleQueue { namespace Uit { DirectoryUpload::DirectoryUpload(Session *session, QObject *parentObject) : FileSystemOperation(session, parentObject) { m_networkAccess = new QNetworkAccessManager(this); connect(m_networkAccess, SIGNAL(finished(QNetworkReply*)), this, SLOT(finished(QNetworkReply*))); } void DirectoryUpload::start() { GetStreamingFileUploadURLRequest *request = new GetStreamingFileUploadURLRequest(m_session, this); connect(request, SIGNAL(finished()), this, SLOT(uploadInternal())); connect(request, SIGNAL(error(const QString &)), this, SLOT(requestError(const QString &))); request->submit(); } void DirectoryUpload::uploadInternal() { GetStreamingFileUploadURLRequest *request = qobject_cast(sender()); if (!request) { QString msg = tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not GetStreamingFileUploadURLRequest!"); Logger::logError(msg, m_job.moleQueueId()); emit error(msg); return; } request->deleteLater(); m_url = request->url(); m_fileEntries.enqueue(QFileInfo(m_localPath)); uploadNext(); } void DirectoryUpload::uploadNext() { if (m_fileEntries.isEmpty()) { emit FileSystemOperation::finished(); return; } QFileInfo path = m_fileEntries.dequeue(); if (path.isDir()) { QFileInfoList entries = QDir(path.absoluteFilePath()) .entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); m_fileEntries.append(entries); QString dir = path.absoluteFilePath().replace(m_localPath, ""); if (!dir.startsWith("/")) dir = "/" + dir; CreateDirectoryRequest *request = new CreateDirectoryRequest(m_session, this); request->setHostId(m_hostID); request->setUserName(m_userName); request->setDirectory(m_remotePath + dir); connect(request, SIGNAL(finished()), this, SLOT(createDirectoryComplete())); connect(request, SIGNAL(error(const QString &)), this, SLOT(requestError(const QString &))); request->submit(); } else { uploadFile(path); } } void DirectoryUpload::createDirectoryComplete() { CreateDirectoryRequest *request = qobject_cast(sender()); if (!request) { QString msg = tr("Internal error: %1\n%2").arg(Q_FUNC_INFO) .arg("Sender is not CreateDirectoryRequest!"); Logger::logError(msg, m_job.moleQueueId()); emit error(msg); return; } request->deleteLater(); uploadNext(); } void DirectoryUpload::uploadFile(const QFileInfo &fileInfo) { FileStreamingData fileData; QString filePath = fileInfo.absoluteFilePath().replace(m_localPath, ""); if (!filePath.startsWith("/")) filePath = "/" + filePath; QString remoteFilePath = m_remotePath + filePath; fileData.setToken(m_session->token()); fileData.setFileName(remoteFilePath); fileData.setUserName(m_userName); fileData.setHostID(m_hostID); QString xml = fileData.toXml(); // Open file and get size QFile *file = new QFile(fileInfo.absoluteFilePath()); if (!file->open(QIODevice::ReadOnly)) { QString msg = tr("Unable to open file: %1") .arg(fileInfo.absoluteFilePath()); Logger::logError(msg, m_job.moleQueueId()); emit error(msg); return; } CompositeIODevice *dataStream = new CompositeIODevice(this); dataStream->open(QIODevice::ReadWrite); QBuffer *headerBuffer = new QBuffer(dataStream); headerBuffer->open(QIODevice::ReadWrite); QTextStream headerStream(headerBuffer); headerStream << xml.size(); headerStream << "|"; headerStream << xml; headerStream << file->size(); headerStream << "|"; headerStream.flush(); headerBuffer->seek(0); dataStream->addDevice(headerBuffer); dataStream->addDevice(file); m_networkAccess->post(QNetworkRequest(QUrl(m_url)), dataStream); } void DirectoryUpload::finished(QNetworkReply *reply) { QByteArray response = reply->readAll(); if (!response.isEmpty() && !QString(response).contains("DONE")) Logger::logError(response, m_job.moleQueueId()); if (reply->error() == QNetworkReply::NoError) uploadNext(); else emit error(reply->errorString()); } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/directoryupload.h000066400000000000000000000042651323436134600240100ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef UITFILEUPLOADER_H_ #define UITFILEUPLOADER_H_ #include "filesystemoperation.h" #include #include #include #include namespace MoleQueue { namespace Uit { class Session; /** * @brief File system operation to upload a directory to a remote UIT system. */ class DirectoryUpload: public FileSystemOperation { Q_OBJECT public: /** * @param session The UIT session. * @param parentObject The parent object. */ DirectoryUpload(Session *session, QObject *parentObject = 0); /** * @return The local path to be uploaded. */ QString localPath() const { return m_localPath; } /** * @param path The local path to be uploaded. */ void setLocalPath(const QString& path) { m_localPath = path; } /** * @return The remote file path for the directory to be uploaded to. */ QString targetPath() const { return m_remotePath; } /** * @param path The target path on the remote system. */ void setRemotePath(const QString& path) { m_remotePath = path; } void start(); private slots: void uploadInternal(); void uploadNext(); void uploadFile(const QFileInfo &fileInfo); void finished(QNetworkReply *reply); void createDirectoryComplete(); private: QString m_localPath; QString m_remotePath; QNetworkAccessManager *m_networkAccess; QString m_url; QQueue m_fileEntries; void uploadFile(const QString &filePath); }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* UITFILEUPLOADER_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/dirlistinginfo.cpp000066400000000000000000000112441323436134600241510ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "queues/uit/dirlistinginfo.h" #include "logger.h" #include "fileinfo.h" #include "messagehandler.h" #include #include #include #include namespace { class FileInfoXmlReceiver : public QAbstractXmlReceiver { public: FileInfoXmlReceiver(QXmlNamePool pool); void atomicValue ( const QVariant & value ) { Q_UNUSED(value); } void attribute ( const QXmlName & name, const QStringRef & value ) { Q_UNUSED(name); Q_UNUSED(value); } void characters ( const QStringRef & value ); void comment ( const QString & value ) { Q_UNUSED(value); } void endDocument () {}; void endElement (); void endOfSequence () {}; void namespaceBinding ( const QXmlName & name ) { Q_UNUSED(name); } void processingInstruction ( const QXmlName & target, const QString & value) { Q_UNUSED(target); Q_UNUSED(value); } void startDocument () {}; void startElement ( const QXmlName & name ); void startOfSequence () {}; QList fileInfos() const { return m_fileInfos; }; private: QXmlNamePool m_pool; MoleQueue::Uit::FileInfo m_currentFileInfo; QString m_currentName; QString m_currentValue; QList m_fileInfos; int tagDepth; }; FileInfoXmlReceiver::FileInfoXmlReceiver(QXmlNamePool pool) : m_pool(pool), tagDepth(0) { } void FileInfoXmlReceiver::characters ( const QStringRef & value ) { if (!m_currentName.isEmpty()) { m_currentValue.append(value); } } void FileInfoXmlReceiver::endElement () { if (tagDepth-- == 2) { m_fileInfos.append(m_currentFileInfo); m_currentFileInfo = MoleQueue::Uit::FileInfo(); } else if (m_currentName == "size") { bool ok; m_currentFileInfo.setSize(m_currentValue.toLongLong(&ok)); if (!ok) { MoleQueue::Logger::logError( QObject::tr("Unable to convert value to qint64: %1") .arg(m_currentValue)); } } else if (m_currentName == "name") { m_currentFileInfo.setName(m_currentValue); } else if (m_currentName == "perms") { m_currentFileInfo.setPerms(m_currentValue); } else if (m_currentName == "date") { m_currentFileInfo.setDate(m_currentValue); } else if (m_currentName == "user") { m_currentFileInfo.setUser(m_currentValue); } else if (m_currentName == "group") { m_currentFileInfo.setGroup(m_currentValue); } m_currentValue.clear(); m_currentName.clear(); } void FileInfoXmlReceiver::startElement ( const QXmlName & name ) { tagDepth++; m_currentName = name.localName(m_pool); m_currentValue.clear(); } } /* namespace anonymous */ namespace MoleQueue { namespace Uit { DirListingInfo::DirListingInfo() : m_valid(false) { // TODO Auto-generated constructor stub } DirListingInfo::DirListingInfo(const DirListingInfo &other) : m_valid(other.isValid()), m_currentDirectory(other.currentDirectory()), m_directories(other.directories()), m_files(other.files()) { } DirListingInfo DirListingInfo::fromXml(const QString &xml) { DirListingInfo info; info.setContent(xml); return info; } void DirListingInfo::setContent(const QString &content) { m_xml = content; m_valid = true; MessageHandler handler; QXmlQuery query; query.setMessageHandler(&handler); query.setFocus(m_xml); QString dir; query.setQuery("/DirListingInfo/currentDirectory/string()"); m_valid = query.evaluateTo(&dir); if (!m_valid) return; m_currentDirectory = dir.trimmed(); // Get the directories; FileInfoXmlReceiver dirReceiver(query.namePool()); query.setQuery("/DirListingInfo/directories"); m_valid = query.evaluateTo(&dirReceiver); if (!m_valid) return; m_directories = dirReceiver.fileInfos(); // Get the files FileInfoXmlReceiver fileReceiver(query.namePool()); query.setQuery("/DirListingInfo/files"); m_valid = query.evaluateTo(&fileReceiver); if (!m_valid) return; m_files = fileReceiver.fileInfos(); } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/dirlistinginfo.h000066400000000000000000000051611323436134600236170ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef DIRLISTINGINFO_H_ #define DIRLISTINGINFO_H_ #include "fileinfo.h" #include #include namespace MoleQueue { namespace Uit { /** * @brief class used to model UIT DirListingInfo. */ class DirListingInfo { public: DirListingInfo(); /** * @param other The instance being copied. */ DirListingInfo(const DirListingInfo &other); /** * Convert XML representation into object model. * * @return The DirListingInfo instance representing the XML * @param xml The XML to parse. */ static DirListingInfo fromXml(const QString &xml); /** * @return The current directory. */ QString currentDirectory() const { return m_currentDirectory; } /** * @param current The current directory. */ void setCurrentDirectory(const QString& current) { m_currentDirectory = current; } /** * @return The list of directories. */ const QList& directories() const { return m_directories; } /** * @param dirs The list of directories. */ void setDirectories(const QList& dirs) { m_directories = dirs; } /** * @return The list of files. */ const QList& files() const { return m_files; } /** * @param files The list of files. */ void setFiles(const QList& fs) { m_files = fs; } /** * @returns true if this DirListingInfo object represents a valid XML * document, false otherwise. */ bool isValid() const { return m_valid; } /** * @return The raw XML used to generate this object. */ QString xml() { return m_xml; } private: bool m_valid; QString m_currentDirectory; QList m_directories; QList m_files; QString m_xml; /** * Parse the XML and set the fields using the data in the XML document. * * @param xml The XML document to parse. */ void setContent(const QString &xml); }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* DIRLISTINGINFO_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/fileinfo.cpp000066400000000000000000000014371323436134600227230ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "queues/uit/fileinfo.h" namespace MoleQueue { namespace Uit { FileInfo::FileInfo() : m_size(-1) { } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/fileinfo.h000066400000000000000000000045001323436134600223620ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef FILEINFO_H_ #define FILEINFO_H_ #include namespace MoleQueue { namespace Uit { /** * @brief class used to model UIT FileInfo. */ class FileInfo { public: FileInfo(); /** * @return The date string for this file. */ QString date() const { return m_date; } /** * @param d The date string. */ void setDate(const QString& d) { m_date = d; } /** * @return The string describing the group the file belongs to. */ QString group() const { return m_group; } /** * @param g The string describing the group the file belongs to. */ void setGroup(const QString& g) { m_group = g; } /** * @return The name of the file. */ QString name() const { return m_name; } /** * @param n The name of the file. */ void setName(const QString& n) { m_name = n; } /** * @return The permissions of the file. */ QString perms() const { return m_perms; } /** * @param p The string describing the permssion for this file. */ void setPerms(const QString& p) { m_perms = p; } /** * @return The size of the file. */ qint64 size() const { return m_size; } /** * @param s The size of the file. */ void setSize(qint64 s) { m_size = s; } /** * @return The user who owns the file. */ QString user() const { return m_user; } /** * @param u The user who owns this file. */ void setUser(const QString& u) { m_user = u; } private: qint64 m_size; QString m_name; QString m_perms; QString m_date; QString m_user; QString m_group; }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* FILEINFO_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/filestreamingdata.cpp000066400000000000000000000024341323436134600246110ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include "filestreamingdata.h" namespace MoleQueue { namespace Uit { FileStreamingData::FileStreamingData() : m_hostID(-1) { } QString FileStreamingData::toXml() const { QString xml; QXmlStreamWriter xmlWriter(&xml); xmlWriter.writeStartDocument(); xmlWriter.writeStartElement("FileStreamingData"); xmlWriter.writeTextElement("token", m_token); xmlWriter.writeTextElement("filename", m_fileName); xmlWriter.writeTextElement("hostID", QString::number(m_hostID)); xmlWriter.writeTextElement("username", m_userName); xmlWriter.writeEndElement(); return xml; } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/filestreamingdata.h000066400000000000000000000044251323436134600242600ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef FILESTREAMINGDATA_H_ #define FILESTREAMINGDATA_H_ #include namespace MoleQueue { namespace Uit { /** * @brief class used to model UIT FileStreamingData XML document. * Used to describe a file on a remote UIT system during the * download/upload process. */ class FileStreamingData { public: FileStreamingData(); /** * @return The XML document generated using the fields in this * instance. This document can be POSTed to the UIT interface. */ QString toXml() const; /** * @return The file name. */ QString fileName() const { return m_fileName; } /** * @param file The file name. */ void setFileName(const QString& file) { m_fileName = file; } /** * @return The host ID for the host this file is associated with. */ qint64 hostID() const { return m_hostID; } /** * @param hostId The host ID for the host the file is associatd with. */ void setHostID(qint64 hostId) { m_hostID = hostId; } /** * @return The UIT session token. */ QString token() const { return m_token; } /** * @param tok The UIT session token to use. */ void setToken(const QString& tok) { m_token = tok; } /** * @return The user name for user which owns this file. */ QString userName() const { return m_userName; } /** * @param user The user name for the user which owns this file. */ void setUserName(const QString& user) { m_userName = user; } private: QString m_token; QString m_fileName; qint64 m_hostID; QString m_userName; }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* FILESTREAMINGDATA_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/filesystemoperation.cpp000066400000000000000000000023631323436134600252340ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "filesystemoperation.h" #include "requests.h" namespace MoleQueue { namespace Uit { const QString FileSystemOperation::noSuchFileOrDir = "DIR_LISTING Failed: No such file"; FileSystemOperation::FileSystemOperation(Session *session, QObject *parentObject) : QObject(parentObject), m_session(session), m_hostID(-1) { } void FileSystemOperation::requestError(const QString &errorString) { Request *request = qobject_cast(sender()); if (request) request->deleteLater(); emit error(errorString); } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/filesystemoperation.h000066400000000000000000000056031323436134600247010ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef UITOPERATION_H_ #define UITOPERATION_H_ #include "job.h" #include namespace MoleQueue { namespace Uit { class Session; /** * @brief abstract base class of UIT file system operations. * * See DirectoryDownload and DirectoryUpload for examples of concrete * implementations. */ class FileSystemOperation: public QObject { Q_OBJECT public: /** * @param session The UIT session to use for this operation. * @param parentObject The parent object. */ FileSystemOperation(Session *session, QObject *parentObject = 0); /** * @return The host ID for the host this operation associated with. */ qint64 hostId() const { return m_hostID; } /** * @param id The host ID for the host this operation is associated with. */ void setHostId(qint64 id) { m_hostID = id; } /** * @return The user name of the user performing this file system operation. */ QString userName() const { return m_userName; } /** * @param user The user name of the user performing this file system * operation. */ void setUserName(const QString& user) { m_userName = user; } /** * @return The MoleQueue job this operation is associated with. */ const Job& job() const { return m_job; } /** * @param j The MoleQueue job this operation is associated with. */ void setJob(const Job& j) { this->m_job = j; } /** * Implemented by subclasses, start the operation. */ virtual void start() = 0; /** * Error string produced by UIT statFile(...) when an file/directory doesn't * exist. */ static const QString noSuchFileOrDir; signals: /** * Emitted when the operation is complete. */ void finished(); /** * Emitted if an error occurs during the operation. * * @param errorString The error string describing the error. */ void error(const QString &errorString); protected slots: /** * Slot called when an error occurs while performing the operation. * * @param errorString String describing the error. */ virtual void requestError(const QString &errorString); protected: Session *m_session; qint64 m_hostID; QString m_userName; Job m_job; }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* UITOPERATION_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/jobevent.cpp000066400000000000000000000026721323436134600227460ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "queues/uit/jobevent.h" namespace MoleQueue { namespace Uit { JobEvent::JobEvent() : m_eventTime(-1), m_jobID(-1) { } JobEvent::JobEvent(const JobEvent &other) : m_acctHost(other.acctHost()), m_eventType(other.eventType()), m_eventTime(other.eventTime()), m_jobID(other.jobId()), m_jobQueue(other.jobQueue()), m_jobStatus(other.jobStatus()), m_jobStatusText(other.jobStatusText()) { } JobEvent &JobEvent::operator=( const JobEvent &other) { if (this != &other) { m_acctHost = other.acctHost(); m_eventType = other.eventType(); m_eventTime = other.eventTime(); m_jobID = other.jobId(); m_jobQueue = other.jobQueue(); m_jobStatus = other.jobStatus(); m_jobStatusText = other.jobStatusText(); } return *this; } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/jobevent.h000066400000000000000000000056411323436134600224120ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef JOBEVENT_H_ #define JOBEVENT_H_ #include namespace MoleQueue { namespace Uit { /** * @brief class using to model UIT JobEvent. */ class JobEvent { public: JobEvent(); /** * @param other The JobEvent instance being copied. */ JobEvent(const JobEvent &other); /** * @param other The JobEvent instance being assigned. */ JobEvent &operator=(const JobEvent &other); /** * @return The account host. */ QString acctHost() const { return m_acctHost; } /** * @param host The account host. */ void setAcctHost(const QString& host) { m_acctHost = host; } /** * @return The event time for this job event. */ qint64 eventTime() const { return m_eventTime; } /** * @param time The event time. */ void setEventTime(qint64 time) { m_eventTime = time; } /** * @return The event type for this job event */ QString eventType() const { return m_eventType; } /** * @param type The event type. */ void setEventType(const QString& type) { m_eventType = type; } /** * @return The job ID for this job event. */ qint64 jobId() const { return m_jobID; } /** * @param id The job ID. */ void setJobId(qint64 id) { m_jobID = id; } /** * @return The job queue associated with this job event. */ QString jobQueue() const { return m_jobQueue; } /** * @param queue The queue name. */ void setJobQueue(const QString& queue) { m_jobQueue = queue; } /** * @return The job status associated with this job event. */ QString jobStatus() const\ { return m_jobStatus; } /** * @param status The job status. */ void setJobStatus(const QString &status) { m_jobStatus = status; } /** * @return The job status text associated with this job event. */ QString jobStatusText() const { return m_jobStatusText; } /** * @param text The job status text. */ void setJobStatusText(const QString& text) { m_jobStatusText = text; } private: QString m_acctHost; QString m_eventType; qint64 m_eventTime; qint64 m_jobID; QString m_jobQueue; QString m_jobStatus; QString m_jobStatusText; }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* JOBEVENT_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/jobeventlist.cpp000066400000000000000000000114631323436134600236400ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "jobeventlist.h" #include "messagehandler.h" #include #include #include #include #include namespace { /** * @brief XML receiver to parser JobEvent tags. */ class JobEventListXmlReceiver : public QAbstractXmlReceiver { public: JobEventListXmlReceiver(QXmlNamePool pool); void atomicValue ( const QVariant & value ) { Q_UNUSED(value); } void attribute ( const QXmlName & name, const QStringRef & value ) { Q_UNUSED(name); Q_UNUSED(value); } void characters ( const QStringRef & value ); void comment ( const QString & value ) { Q_UNUSED(value); } void endDocument () {} void endElement (); void endOfSequence () {}; void namespaceBinding ( const QXmlName & name ) { Q_UNUSED(name); }; void processingInstruction ( const QXmlName & target, const QString & value) { Q_UNUSED(target); Q_UNUSED(value); }; void startDocument () {}; void startElement ( const QXmlName & name ); void startOfSequence () {}; QList jobEvents() const { return m_events; }; private: QXmlNamePool m_pool; MoleQueue::Uit::JobEvent m_currentEvent; QString m_currentName; QString m_currentValue; QList m_events; int tagDepth; }; JobEventListXmlReceiver::JobEventListXmlReceiver(QXmlNamePool pool) : m_pool(pool), tagDepth(0) { } void JobEventListXmlReceiver::characters ( const QStringRef & value ) { if (!m_currentName.isEmpty()) { m_currentValue.append(value); } } void JobEventListXmlReceiver::endElement () { if (tagDepth-- == 1) { // Save the current event. m_events.append(m_currentEvent); m_currentEvent = MoleQueue::Uit::JobEvent(); } else if (m_currentName == "acctHost") { m_currentEvent.setAcctHost(m_currentValue); } else if (m_currentName == "eventType") { m_currentEvent.setEventType(m_currentValue); } else if (m_currentName == "eventTime") { m_currentEvent.setEventTime(m_currentValue.toInt()); } else if (m_currentName == "jobID") { QRegExp regex ("^(\\d+)\\..*$"); regex.indexIn(m_currentValue.trimmed()); m_currentEvent.setJobId(regex.cap(1).toLongLong()); } else if (m_currentName == "jobQueue") { m_currentEvent.setJobQueue(m_currentValue); } else if (m_currentName == "jobStatus") { m_currentEvent.setJobStatus(m_currentValue); } else if (m_currentName == "jobStatusText") { m_currentEvent.setJobStatusText(m_currentValue); } m_currentName.clear(); m_currentValue.clear(); } void JobEventListXmlReceiver::startElement ( const QXmlName & name ) { tagDepth++; m_currentName = name.localName(m_pool); m_currentValue.clear(); } } /* namespace anonymous */ namespace MoleQueue { namespace Uit { JobEventList::JobEventList() : m_valid(false) { } JobEventList::JobEventList(const JobEventList &other) : m_valid(false), m_jobEvents(other.jobEvents()) { } JobEventList JobEventList::fromXml(const QString &xml) { QList empty; return fromXml(xml, "", empty); } JobEventList JobEventList::fromXml(const QString &xml, const QString &userName, QList jobIds) { JobEventList list; list.setContent(xml, userName, jobIds); return list; } void JobEventList::setContent(const QString &content, const QString &userName, QList jobIds) { m_xml = content; m_valid = true; MessageHandler handler; QXmlQuery query; query.setMessageHandler(&handler); JobEventListXmlReceiver receiver(query.namePool()); query.setFocus(m_xml); if (jobIds.isEmpty()) { query.setQuery("/list/JobEvent"); } else { QString xpath = "/list/JobEvent/jobID["; QListIterator iter(jobIds); while (iter.hasNext()) { qint64 jobId = iter.next(); xpath += QString("starts-with(text(), '%1')").arg(jobId); if (iter.hasNext()) xpath += " or "; } xpath += "]/parent::node()"; query.setQuery(xpath); } m_valid = query.evaluateTo(&receiver); m_jobEvents = receiver.jobEvents(); } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/jobeventlist.h000066400000000000000000000042651323436134600233070ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef JOBEVENTLIST_H_ #define JOBEVENTLIST_H_ #include "jobevent.h" #include namespace MoleQueue { namespace Uit { /** * @brief class used to model a UIT JobEvent list. */ class JobEventList { public: JobEventList(); /** * @param other The instance to copy. */ JobEventList(const JobEventList &other); /** * @return The list of JobEvents. */ QList jobEvents() const { return m_jobEvents; } /** * @return true, if the instance represents a valid JobEventList document, * false otherwise. */ bool isValid() const { return m_valid; } /** * @return The raw XML that this object was generated from. */ QString xml() const { return m_xml; } /** * Converts JobEventList XML document into object model. * * @return The object model. * @param xml The XML document to parse. */ static JobEventList fromXml(const QString &xml); /** * Converts JobEventList XML document into object model. * * @return The object model. * @param xml The XML document to parse. * @param userName The username to filter JobEvents on. * @param jobIds The job ids to filer JobEvents on. */ static JobEventList fromXml(const QString &xml, const QString &userName, QList jobIds); private: bool m_valid; QList m_jobEvents; QString m_xml; void setContent(const QString &xml, const QString &userName, QList jobIds); }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* JOBEVENTLIST_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/jobsubmissioninfo.cpp000066400000000000000000000053331323436134600246710ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "jobsubmissioninfo.h" #include "messagehandler.h" #include #include namespace MoleQueue { namespace Uit { JobSubmissionInfo::JobSubmissionInfo() : m_valid(false), m_jobNumber(-1) { } JobSubmissionInfo::JobSubmissionInfo(const JobSubmissionInfo &other) : m_valid(other.isValid()), m_jobNumber(other.jobNumber()), m_stdout(other.stdout()), m_stderr(other.stderr()) { } JobSubmissionInfo &JobSubmissionInfo::operator=(const JobSubmissionInfo &other) { if (this != &other) { m_valid = other.isValid(); m_jobNumber = other.jobNumber(); m_stdout = other.stdout(); m_stderr = other.stderr(); } return *this; } bool JobSubmissionInfo::isValid() const { return m_valid; } qint64 JobSubmissionInfo::jobNumber() const { return m_jobNumber; } QString JobSubmissionInfo::stdout() const { return m_stdout; } QString JobSubmissionInfo::stderr() const { return m_stderr; } JobSubmissionInfo JobSubmissionInfo::fromXml(const QString &xml) { JobSubmissionInfo info; info.setContent(xml); return info; } void JobSubmissionInfo::setContent(const QString &content) { m_xml = content; m_valid = true; MessageHandler handler; QXmlQuery query; query.setMessageHandler(&handler); m_valid = query.setFocus(m_xml); if (!m_valid) return; query.setQuery("/JobSubmissionInfo/jobNumber/string()"); QString jobNum; m_valid = query.evaluateTo(&jobNum); if (!m_valid) return; // jobNumber is of the form .sdb, so parse out number. QRegExp regex ("^(\\d+)\\..*$"); int index = regex.indexIn(jobNum.trimmed()); if (index != -1) m_jobNumber = regex.cap(1).toLongLong(); query.setQuery("/JobSubmissionInfo/stdout/string()"); QString out; m_valid = query.evaluateTo(&out); if (!m_valid) return; m_stdout = out.trimmed(); query.setQuery("/JobSubmissionInfo/stderr/string()"); QString err; m_valid = query.evaluateTo(&err); if (!m_valid) return; m_stderr = err.trimmed(); } QString JobSubmissionInfo::xml() const { return m_xml; } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/jobsubmissioninfo.h000066400000000000000000000043221323436134600243330ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef JOBSUBMISSIONINFO_H_ #define JOBSUBMISSIONINFO_H_ #include namespace MoleQueue { namespace Uit { /** * @brief class used to model the response message returned by a UIT server in * response to a job submission. */ class JobSubmissionInfo { public: JobSubmissionInfo(); /** * @param other The instance to copy. */ JobSubmissionInfo(const JobSubmissionInfo &other); /** * @param other The instance to assign. */ JobSubmissionInfo &operator=(const JobSubmissionInfo &other); /** * @return true if this object represents a valid JobSubmissionInfo * document, false otherwise. */ bool isValid() const; /** * @return the job number. */ qint64 jobNumber() const; /** * @return the STDOUT produced by submitting the job. */ QString stdout() const; /** * @return the STDERR produced by submitting the job. */ QString stderr() const; /** * @param xml The XML to parse and populate the object model with. */ void setContent(const QString &xml); /** * @return The raw XML parse and used to populare the object model with. */ QString xml() const; /** * Convert a JobSubmissionInfo XML document into object model. * * @return The JobSubmissionInfo object representing the XML. * @param xml The JobSubmissionInfo XML document to parse. */ static JobSubmissionInfo fromXml(const QString &xml); private: bool m_valid; qint64 m_jobNumber; QString m_stdout; QString m_stderr; QString m_xml; }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* JOBSUBMISSIONINFO_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/kerberoscredentials.cpp000066400000000000000000000024551323436134600251630ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "kerberoscredentials.h" #include namespace MoleQueue { namespace Uit { KerberosCredentials::KerberosCredentials(const QString &principle, const QString &password) : m_principle(principle), m_password(password) { } QString KerberosCredentials::toXml() const { QString xml; QXmlStreamWriter xmlWriter(&xml); xmlWriter.writeStartDocument(); xmlWriter.writeStartElement("KerberosCredentials"); xmlWriter.writeTextElement("principal", m_principle); xmlWriter.writeTextElement("password", m_password); xmlWriter.writeEndElement(); return xml; } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/kerberoscredentials.h000066400000000000000000000024111323436134600246200ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef KERBEROSCREDENTIALS_H_ #define KERBEROSCREDENTIALS_H_ #include namespace MoleQueue { namespace Uit { /** * @brief class used to model UIT KerberosCredentials */ class KerberosCredentials { public: /** * @param principle The Kerberos username. * @param password The Kerberos password. */ KerberosCredentials(const QString &principle, const QString &password); /** * @return The XML message for this KerberosCredentials instance. */ QString toXml() const; private: QString m_principle; QString m_password; }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* KERBEROSCREDENTIALS_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/messagehandler.cpp000066400000000000000000000023121323436134600241030ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "messagehandler.h" #include "logger.h" namespace MoleQueue { namespace Uit { MessageHandler::MessageHandler(QObject *parentObject) : QAbstractMessageHandler(parentObject) { } void MessageHandler::handleMessage(QtMsgType type, const QString &description, const QUrl &identifier, const QSourceLocation &sourceLocation) { Q_UNUSED(type); Q_UNUSED(identifier); Q_UNUSED(sourceLocation); Logger::logError("UIT XML parse error: " + description); } } /* namespace MoleQueue */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/messagehandler.h000066400000000000000000000024521323436134600235550ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MESSAGEHANDLER_H_ #define MESSAGEHANDLER_H_ #include namespace MoleQueue { namespace Uit { /** * Concrete QAbstractMessageHandler implementation used to report errors * associated with parsing XML content past QXmlQuery objects. */ class MessageHandler : public QAbstractMessageHandler { public: MessageHandler(QObject *parentObject = 0); protected: virtual void handleMessage(QtMsgType type, const QString &description, const QUrl &identifier, const QSourceLocation &sourceLocation); }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* MESSAGEHANDLER_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/queueuit.cpp000066400000000000000000000115561323436134600230010ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "queueuit.h" #include "kerberoscredentials.h" #include "sslsetup.h" #include "uitauthenticator.h" #include "job.h" #include "jobmanager.h" #include "logentry.h" #include "logger.h" #include "program.h" #include "uitqueuewidget.h" #include "server.h" #include "credentialsdialog.h" #include "logger.h" #include "mainwindow.h" #include #include #include #include #include #include #include #include namespace MoleQueue { const QString QueueUit::clientId = "0adc5b59-5827-4331-a544-5ba7922ec2b8"; QueueUit::QueueUit(QueueManager *parentObject) : QueueRemote("ezHPC UIT", parentObject), m_dialogParent(NULL) { // ensure SSL certificates are loaded SslSetup::init(); } QueueUit::~QueueUit() { } bool QueueUit::writeJsonSettings(QJsonObject &json, bool exportOnly, bool includePrograms) const { if (!QueueRemote::writeJsonSettings(json, exportOnly, includePrograms)) return false; json["kerberosPrinciple"] = m_kerberosPrinciple; json["kerberosHostName"] = m_hostName; return true; } bool QueueUit::readJsonSettings(const QJsonObject &json, bool importOnly, bool includePrograms) { // Validate JSON: if ((!json["kerberosPrinciple"].isString() || !json["kerberosHostName"].isString())) { Logger::logError(tr("Error reading queue settings: Invalid format:\n%1") .arg(QJsonDocument(json).toJson().constData())); return false; } if (!QueueRemote::readJsonSettings(json, importOnly, includePrograms)) return false; m_kerberosPrinciple = json["kerberosPrinciple"].toString(); m_hostName = json["kerberosHostName"].toString(); return true; } bool QueueUit::testConnection(QWidget *parentObject) { UitAuthenticator *authenticator = new UitAuthenticator( &m_uit, m_kerberosPrinciple, this, parentObject); m_dialogParent = parentObject; connect(authenticator, SIGNAL(authenticationComplete(const QString&)), this, SLOT(testConnectionComplete(const QString&))); connect(authenticator, SIGNAL(authenticationError(const QString&)), this, SLOT(testConnectionError(const QString&))); authenticator->authenticate(); return true; } void QueueUit::testConnectionComplete(const QString &token) { UitAuthenticator *auth = qobject_cast(sender()); if (auth) auth->deleteLater(); QMessageBox::information(m_dialogParent, tr("Success"), tr("Connection to UIT succeeded!")); Q_UNUSED(token); } void QueueUit::testConnectionError(const QString &errorMessage) { UitAuthenticator *auth = qobject_cast(sender()); if (auth) auth->deleteLater(); QMessageBox::critical(m_dialogParent, tr("UIT Error"), errorMessage); } AbstractQueueSettingsWidget* QueueUit::settingsWidget() { UitQueueWidget *widget = new UitQueueWidget (this); return widget; } void QueueUit::createRemoteDirectory(Job job) { Q_UNUSED(job); // TODO } void QueueUit::remoteDirectoryCreated() { // TODO } void QueueUit::copyInputFilesToHost(Job job) { Q_UNUSED(job); //TODO } void QueueUit::inputFilesCopied() { // TODO } void QueueUit::submitJobToRemoteQueue(Job job) { Q_UNUSED(job); // TODO } void QueueUit::jobSubmittedToRemoteQueue() { // TODO } void QueueUit::requestQueueUpdate() { // TODO } void QueueUit::handleQueueUpdate() { // TODO } void QueueUit::beginFinalizeJob(IdType queueId) { Q_UNUSED(queueId); // TODO } void QueueUit::finalizeJobCopyFromServer(Job job) { Q_UNUSED(job); // TODO } void QueueUit::finalizeJobOutputCopiedFromServer() { // TODO } void QueueUit::finalizeJobCopyToCustomDestination(Job job) { Q_UNUSED(job); // TODO } void QueueUit::finalizeJobCleanup(Job job) { Q_UNUSED(job); // TODO } void QueueUit::cleanRemoteDirectory(Job job) { Q_UNUSED(job); // TODO } void QueueUit::remoteDirectoryCleaned() { // TODO } void QueueUit::beginKillJob(Job job) { Q_UNUSED(job); // TODO } void QueueUit::endKillJob() { // TODO } } // End namespace molequeue-0.9.0/molequeue/app/queues/uit/requests.cpp000066400000000000000000000204701323436134600230010ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "wsdl_uitapi.h" #include "requests.h" #include "session.h" namespace MoleQueue { namespace Uit { Request::Request(Session *session, QObject *parentObject) : QObject(parentObject), m_session(session), m_hostID(-1) { } void Request::submit() { KDSoapJob *job = createJob(); connect(job, SIGNAL(finished(KDSoapJob *)), this, SLOT(finished(KDSoapJob *))); job->start(); } void Request::finished(KDSoapJob *job) { m_response = job->reply(); // UIT error case if (job->isFault()) { processFault(m_response); // Process response to submit } else { emit finished(); } } void Request::processFault(const KDSoapMessage &fault) { if (isTokenError(fault)) { m_session->authenticate(this, SLOT(submit()), this, SIGNAL(error(const QString))); } else { emit error(fault.faultAsString()); } } bool Request::isTokenError(const KDSoapMessage& fault) { return fault.arguments().child("faultstring").value().toString() == "java.lang.Exception: Invalid Token"; } SubmitBatchScriptJobRequest::SubmitBatchScriptJobRequest(Session *session, QObject *parentObject) : Request(session, parentObject) { } KDSoapJob * SubmitBatchScriptJobRequest::createJob() { SubmitBatchScriptJobJob *soapJob = new SubmitBatchScriptJobJob( m_session->uitService(), this); soapJob->setToken(m_session->token()); soapJob->setHostID(m_hostID); soapJob->setBatchScript("job.uit"); soapJob->setWorkingDir(m_workingDir); soapJob->setUsername(m_userName); return soapJob; } JobSubmissionInfo SubmitBatchScriptJobRequest::jobSubmissionInfo() { QString responseXml = m_response.value().value(); JobSubmissionInfo info = JobSubmissionInfo::fromXml(responseXml); return info; } GetUserHostAssocRequest::GetUserHostAssocRequest(Session *session, QObject *parentObject) : Request(session, parentObject) { } KDSoapJob * GetUserHostAssocRequest::createJob() { GetUserHostAssocJob *soapJob = new GetUserHostAssocJob( m_session->uitService(), this); soapJob->setToken(m_session->token()); return soapJob; } UserHostAssocList GetUserHostAssocRequest::userHostAssocList() { QString responseXml = m_response.value().value(); UserHostAssocList info = UserHostAssocList::fromXml(responseXml); return info; } CreateDirectoryRequest::CreateDirectoryRequest(Session *session, QObject *parentObject) : Request(session, parentObject) { } KDSoapJob * CreateDirectoryRequest::createJob() { CreateDirectoryJob *soapJob = new CreateDirectoryJob( m_session->uitService(), this); soapJob->setToken(m_session->token()); soapJob->setHostID(m_hostID); soapJob->setUsername(m_userName); soapJob->setDirectory(m_directory); return soapJob; } GetStreamingFileUploadURLRequest::GetStreamingFileUploadURLRequest( Session *session, QObject *parentObject) : Request(session, parentObject) { } KDSoapJob * GetStreamingFileUploadURLRequest::createJob() { GetStreamingFileUploadURLJob *soapJob = new GetStreamingFileUploadURLJob( m_session->uitService(), this); soapJob->setToken(m_session->token()); return soapJob; } QString GetStreamingFileUploadURLRequest::url() { return m_response.value().value(); } GetJobsForHostForUserByNumDaysRequest::GetJobsForHostForUserByNumDaysRequest( Session *session, QObject *parentObject) : Request(session, parentObject) { } KDSoapJob * GetJobsForHostForUserByNumDaysRequest::createJob() { GetJobsForHostForUserByNumDaysJob *soapJob = new GetJobsForHostForUserByNumDaysJob(m_session->uitService(), this); soapJob->setToken(m_session->token()); soapJob->setHostID(m_hostID); soapJob->setSearchUser(m_searchUser); soapJob->setUsername(m_userName); soapJob->setNumDays(m_numDays); return soapJob; } JobEventList GetJobsForHostForUserByNumDaysRequest::jobEventList( QList jobIds) { QString responseXml = m_response.value().value(); JobEventList list = JobEventList::fromXml(responseXml, m_searchUser, jobIds); return list; } GetStreamingFileDownloadURLRequest::GetStreamingFileDownloadURLRequest( Session *session, QObject *parentObject) : Request(session, parentObject) { } KDSoapJob * GetStreamingFileDownloadURLRequest::createJob() { GetStreamingFileDownloadURLJob *soapJob = new GetStreamingFileDownloadURLJob( m_session->uitService(), this); soapJob->setToken(m_session->token()); return soapJob; } QString GetStreamingFileDownloadURLRequest::url() { return m_response.value().value(); } GetDirectoryListingRequest::GetDirectoryListingRequest(Session *session, QObject *parentObject) : Request(session, parentObject) { } KDSoapJob * GetDirectoryListingRequest::createJob() { GetDirectoryListingJob *soapJob = new GetDirectoryListingJob( m_session->uitService(), this); soapJob->setToken(m_session->token()); soapJob->setHostID(m_hostID); soapJob->setUsername(m_userName); soapJob->setDirectory(m_directory); return soapJob; } DirListingInfo GetDirectoryListingRequest::dirListingInfo() { QString responseXml = m_response.value().value(); DirListingInfo info = DirListingInfo::fromXml(responseXml); return info; } DeleteFileRequest::DeleteFileRequest(Session *session, QObject *parentObject) : Request(session, parentObject) { } KDSoapJob * DeleteFileRequest::createJob() { DeleteFileJob *soapJob = new DeleteFileJob( m_session->uitService(), this); soapJob->setToken(m_session->token()); soapJob->setHostID(m_hostID); soapJob->setUsername(m_userName); soapJob->setFile(m_file); return soapJob; } DeleteDirectoryRequest::DeleteDirectoryRequest(Session *session, QObject *parentObject) : Request(session, parentObject) { } KDSoapJob * DeleteDirectoryRequest::createJob() { DeleteDirectoryJob *soapJob = new DeleteDirectoryJob( m_session->uitService(), this); soapJob->setToken(m_session->token()); soapJob->setHostID(m_hostID); soapJob->setUsername(m_userName); soapJob->setDirectory(m_directory); return soapJob; } CancelJobRequest::CancelJobRequest(Session *session, QObject *parentObject) : Request(session, parentObject) { } KDSoapJob * CancelJobRequest::createJob() { CancelJobJob *soapJob = new CancelJobJob(m_session->uitService(), this); soapJob->setToken(m_session->token()); soapJob->setHostID(m_hostID); soapJob->setUsername(m_userName); soapJob->setJobID(QString::number(m_job.queueId())); return soapJob; } StatFileRequest::StatFileRequest(Session *session, QObject *parentObject) : Request(session, parentObject) { } KDSoapJob * StatFileRequest::createJob() { StatFileJob *soapJob = new StatFileJob(m_session->uitService(), this); soapJob->setToken(m_session->token()); soapJob->setHostID(m_hostID); soapJob->setUsername(m_userName); soapJob->setFilename(m_filename); return soapJob; } QString StatFileRequest::output() { QString statOutput = m_response.value().value(); return statOutput; } bool StatFileRequest::exists() { return output().contains("No such file or directory"); } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/requests.h000066400000000000000000000300101323436134600224350ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef REQUESTS_H_ #define REQUESTS_H_ #include "job.h" #include "jobsubmissioninfo.h" #include "userhostassoclist.h" #include "wsdl_uitapi.h" #include "jobeventlist.h" #include "dirlistinginfo.h" #include class KDSoapJob; namespace MoleQueue { namespace Uit { class Session; /** * @brief Abstract base class of all UIT SOAP requests. */ class Request : public QObject { Q_OBJECT public: /** * @param session The UIT session. * @param parentObject The parent object. */ Request(Session *session, QObject *parentObject = 0); public slots: /** * Slot used to submit request to UIT server. */ void submit(); /** * @return the host ID of the UIT host this request is associated with. */ qint64 hostId() const { return m_hostID; } /** * @param id The host ID. */ void setHostId(qint64 id) { m_hostID = id; } /** * @return The user name this request is associated with. */ QString userName() const { return m_userName; } /** * @param user The user name this request should be associated with. */ void setUserName(const QString& user) { m_userName = user; } signals: /** * Emitted when this request is completed. */ void finished(); /** * Emitted is an error occurs. * * @param errorString The error message. */ void error(const QString &errorString); protected: // The UIT session Session *m_session; // The SOAP response KDSoapMessage m_response; qint64 m_hostID; QString m_userName; /** * Overridden by subclasses to create appropriate KDSoapJob for this request. */ virtual KDSoapJob *createJob() = 0; protected slots: /** * Slot called by KDSoap when the SOAP request is complete. * * @param job The SOAP request that just completed. */ void finished(KDSoapJob *job); private: /** * @return true, if the fail was the result of a invalid token ( we need to * authenticate ), false otherwise. */ bool isTokenError(const KDSoapMessage& fault); /** * Process a SOAP fault message. * * @param fault The fault message to process. */ void processFault(const KDSoapMessage& fault); /** * Process the SOAP reply. * * @param reply The SOAP reply from the UIT server. */ void processReply(const KDSoapMessage& reply); }; /** * Concrete Request class for submitBatchScriptJob message. */ class SubmitBatchScriptJobRequest: public Request { Q_OBJECT public: SubmitBatchScriptJobRequest(Session *session, QObject *parentObject = 0); /** * @return The Job being submitted. */ Job job() const { return m_job; } void setJob(const Job &j) { m_job = j; } /** * @return The path the batch script to submit. */ QString batchScript() const { return m_batchScript; } /** * @param script The path to the batch script to submit. */ void setBatchScript(const QString& script) { m_batchScript = script; } /** * @return The working directory to submit the job from. */ QString workingDir() { return m_workingDir; } /** * @param dir The working directory to submit the job from. */ void setWorkingDir(const QString& dir) { m_workingDir = dir; } /** * @return The JobSubmissionInfo object representing the response from the * UIT server. * * Note: Will only produce a populated JobSubmissionInfo object after the * finished() signal has been emitted. */ JobSubmissionInfo jobSubmissionInfo(); protected: KDSoapJob *createJob(); private: Job m_job; QString m_batchScript; QString m_workingDir; }; /** * @brief Concrete Request class for getUserHostAssoc message. */ class GetUserHostAssocRequest: public Request { Q_OBJECT public: /** * @param session The UIT session. * @param parentObject The parent object. */ GetUserHostAssocRequest(Session *session, QObject *parentObject = 0); /** * @return The UserHostAssocList object representing the response from the * UIT server. * * Note: Will only produce a populated UserHostAssocList object after the * finished() signal has been emitted. */ UserHostAssocList userHostAssocList(); protected: KDSoapJob *createJob(); }; /** * @brief Concrete Request class for createDirectory message. */ class CreateDirectoryRequest: public Request { Q_OBJECT public: /** * @param session The UIT session. * @param parentObject The parent object. */ CreateDirectoryRequest(Session *session, QObject *parentObject = 0); /** * @return The directory the request is going to create. */ QString directory() const { return m_directory; } /** * @param dir The directory to create. */ void setDirectory(const QString& dir) { m_directory = dir; } /** * @return The job associated with this request. */ Job job() const { return m_job; } /** * @param j The job associated with this request. */ void setJob(const Job& j) { m_job = j; } protected: KDSoapJob *createJob(); private: Job m_job; QString m_directory; }; /** * @brief Concrete Request class for getStreamingFileUploadURL message. */ class GetStreamingFileUploadURLRequest: public Request { Q_OBJECT public: /** * @param session The UIT session. * @param parentObject The parent object. */ GetStreamingFileUploadURLRequest(Session *session, QObject *parentObject = 0); /** * @return The job associated with this request. */ Job job() const { return m_job; } /** * @param j The job associated with this request. */ void setJob(const Job& j) { m_job = j; } /** * @return The file upload URL returned by the server. * * Note: Will only produce valid URL after the * finished() signal has been emitted. * */ QString url(); protected: KDSoapJob *createJob(); private: Job m_job; }; /** * @brief Concrete Request class for getJobsForHostForUserByNumDays message. */ class GetJobsForHostForUserByNumDaysRequest: public Request { Q_OBJECT public: /** * @param session The UIT session. * @param parentObject The parent object. */ GetJobsForHostForUserByNumDaysRequest(Session *session, QObject *parentObject = 0); /** * @return The number of days of events to retrieve. */ qint64 numDays() const { return m_numDays; } /** * @param days The number of days of events to retrieve. */ void setNumDays(qint64 days) { m_numDays = days; } /** * @return The user to filter events by. */ QString searchUser() const { return m_searchUser; } /** * @param user The user to filter events by. */ void setSearchUser(const QString& user) { m_searchUser = user; } /** * @return The JobEventList object representing the response from the * UIT server. * * @param jobIds The list of job ID to filter the events on. * * Note: Will only produce a populated JobEventList object after the * finished() signal has been emitted. */ JobEventList jobEventList(QList jobIds); protected: KDSoapJob *createJob(); private: QString m_searchUser; qint64 m_numDays; }; /** * @brief Concrete Request class for getStreamingFileDownloadURL message. */ class GetStreamingFileDownloadURLRequest: public Request { Q_OBJECT public: /** * @param session The UIT session. * @parentObject The parent object. */ GetStreamingFileDownloadURLRequest(Session *session, QObject *parentObject = 0); /** * @return The file download URL returned by the server. * * Note: Will only produce valid URL after the * finished() signal has been emitted. * */ QString url(); protected: KDSoapJob *createJob(); }; /** * @brief Concrete Request class for getDirectoryListing message. */ class GetDirectoryListingRequest: public Request { Q_OBJECT public: /** * @param session The UIT session. * @param parentObject The parent object. */ GetDirectoryListingRequest(Session *session, QObject *parentObject = 0); /** * @return The directory to get the list of. */ QString directory() const { return m_directory; } /** * @param The directory we want a listing for. */ void setDirectory(const QString& dir) { m_directory = dir; } /** * @return The DirListingInfo object representing the response from the * UIT server. * * @param jobIds The list of job ID to filter the events on. * * Note: Will only produce a populated DirListingInfo object after the * finished() signal has been emitted. */ DirListingInfo dirListingInfo(); protected: KDSoapJob *createJob(); private: QString m_directory; }; /** * @brief Concrete Request class for deleteFileRequest message. */ class DeleteFileRequest: public Request { Q_OBJECT public: /** * @param session The UIT session. * @param parentObject The parent object. */ DeleteFileRequest(Session *session, QObject *parentObject = 0); /** * @return The file being deleted. */ QString file() const { return m_file; } /** * @param f The file to be deleted. */ void setFile(const QString& f) { m_file = f; } protected: KDSoapJob *createJob(); private: QString m_file; }; /** * @brief Concrete Request class for deleteDirectory message. */ class DeleteDirectoryRequest: public Request { Q_OBJECT public: /** * @param session The UIT session. * @param parentObject The parent object. */ DeleteDirectoryRequest(Session *session, QObject *parentObject = 0); /** * @return The directory being deleted. */ QString directory() const { return m_directory; } /** * @param dir The directory to be deleted. */ void setDirectory(const QString& dir) { m_directory = dir; } protected: KDSoapJob *createJob(); private: QString m_directory; }; /** * @brief Concrete Request class for cancelJob message. */ class CancelJobRequest: public Request { Q_OBJECT public: /** * @param session The UIT session. * @param parentObject The parent object. */ CancelJobRequest(Session *session, QObject *parentObject = 0); /** * @return The job associated with this request. */ Job job() const { return m_job; } /** * @param The job associated with this request. */ void setJob(const Job& j) { m_job = j; } protected: KDSoapJob *createJob(); private: Job m_job; }; /** * @brief Concrete Request class for statFile message. */ class StatFileRequest: public Request { Q_OBJECT public: /** * @param session The UIT session. * @param parentObject The parent object. */ StatFileRequest(Session *session, QObject *parentObject = 0); /** * @return The job associated with this request. */ Job job() const { return m_job; } /** * @param The job associated with this request. */ void setJob(const Job& j) { m_job = j; } QString filename() const { return m_filename; } void setFilename(QString name) { m_filename = name; } QString output(); bool exists(); protected: KDSoapJob *createJob(); private: Job m_job; QString m_filename; }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* REQUESTS_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/session.cpp000066400000000000000000000060771323436134600226200ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "session.h" #include "authenticator.h" #include namespace MoleQueue { namespace Uit { Session::Session(const QString &username, const QString &realm, QObject *parentObject) : QObject(parentObject), m_kerberosUserName(username), m_kerberosRealm(realm), m_kerberosPrinciple(username + "@" + realm), m_authenticator(NULL) { } QString Session::token() { return m_token; } UitapiService *Session::uitService() { return &m_uit; } QString Session::kerberosPrinciple() { return m_kerberosPrinciple; } void Session::authenticate(QObject *completeReciever, const char *completeSlot, QObject *errorReceiver, const char *errorSlot) { // Take mutex to serialize authentication within a session. QMutexLocker locker(&m_authMutex); connect(this, SIGNAL(authenticationComplete(const QString&)), completeReciever, completeSlot); connect(this, SIGNAL(authenticationError(const QString&)), errorReceiver, errorSlot); // If we aren't currently authenticating create an Athenticator ... if (!m_authenticator) { m_authenticator = new Authenticator(&m_uit, kerberosPrinciple(), this); connect(m_authenticator, SIGNAL(authenticationComplete(const QString&)), this, SLOT(authenticationCompleteInternal(const QString&))); connect(m_authenticator, SIGNAL(authenticationError(const QString&)), this, SLOT(authenticationErrorInternal(const QString&))); connect(m_authenticator, SIGNAL(authenticationCanceled()), this, SLOT(authenticationCanceledInternal())); m_authenticator->authenticate(); } } void Session::authenticationCompleteInternal(const QString &tok) { QMutexLocker locker(&m_authMutex); m_authenticator->deleteLater(); m_authenticator = NULL; m_token = tok; emit authenticationComplete(tok); disconnect(); } void Session::authenticationErrorInternal(const QString &errorString) { QMutexLocker locker(&m_authMutex); m_authenticator->deleteLater(); m_authenticator = NULL; emit authenticationError(errorString); disconnect(); } void Session::authenticationCanceledInternal() { QMutexLocker locker(&m_authMutex); m_authenticator->deleteLater(); m_authenticator = NULL; emit authenticationError("Authentication process was canceled by user."); disconnect(); } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/session.h000066400000000000000000000050771323436134600222640ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef SESSION_H_ #define SESSION_H_ #include "wsdl_uitapi.h" #include #include namespace MoleQueue { namespace Uit { class Authenticator; /** * @class Session session.h * * @brief The Session class encapsulates a UIT authentication token that can * be share across multiple requests. * */ class Session : public QObject { Q_OBJECT public: /** * @param username The Kerberos user name. * @param realm The Kerbero realm. * @param parentObject The parent object. */ Session(const QString &username, const QString &realm, QObject *parentObject = 0); /** * Called to authenticate the session with the UIT server. * * @param completeReceiver A pointer to the QObject that will receive the * complete() signal when the authentication process is complete. * @param completeSlot A slot on the completeReceiver class that will be * called when the authentication process is complete. * @param errorReceiver A pointer to the QObject that will receive error * signal during the authentication process if an error occurs. */ void authenticate(QObject *completeReceiver, const char *completeSlot, QObject *errorReceiver, const char *errorSlot); QString kerberosPrinciple(); QString token(); UitapiService *uitService(); signals: void authenticationComplete(const QString &token); void authenticationError(const QString &errorString); private slots: void authenticationCompleteInternal(const QString &token); void authenticationErrorInternal(const QString &errorMessage); void authenticationCanceledInternal(); private: QString m_kerberosUserName; QString m_kerberosRealm; QString m_kerberosPrinciple; UitapiService m_uit; QString m_token; Authenticator *m_authenticator; QMutex m_authMutex; }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* SESSION_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/sessionmanager.cpp000066400000000000000000000032241323436134600241420ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "sessionmanager.h" #include "session.h" #include namespace MoleQueue { namespace Uit { SessionManager::SessionManager(QObject *parentObject) : QObject(parentObject) { } SessionManager * SessionManager::instance() { static QMutex mutex; static SessionManager *uitSessionManager; if (!uitSessionManager) { mutex.lock(); if (!uitSessionManager) uitSessionManager = new SessionManager(QCoreApplication::instance()); mutex.unlock(); } return uitSessionManager; } Session * SessionManager::session(const QString &userName, const QString &realm) { QString principle = userName + "@" + realm; Session *sess = m_sessions.value(principle, NULL); if (!sess) { m_sessionsMutex.lock(); sess = m_sessions.value(principle, NULL); if (!sess) { sess = new Session(userName, realm, this); m_sessions.insert(principle, sess); } m_sessionsMutex.unlock(); } return sess; } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/sessionmanager.h000066400000000000000000000043141323436134600236100ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef SESSIONMANAGER_H_ #define SESSIONMANAGER_H_ #include "authenticator.h" #include #include namespace MoleQueue { namespace Uit { class Session; /** * @brief Singleton to manages create and access to UIT sessions. Sessions are key on * Kerberos user name and realm. */ class SessionManager: public QObject { Q_OBJECT public: /** * @return The single instance of the manager. */ static SessionManager * instance(); /** * Lookup a particular UIT session, creating a new session if necessary. */ Session * session(const QString &userName, const QString &realm); signals: /** * Emitted when a session token is available for a session ( i.e. the * authentication process is complete. * * @param token The UIT session token. */ void sessionToken(const QString &token); /** * Emitted when a error occures will create a UIT session or during the * authentication process. * * @param String describing the error that occurred. */ void requestSessionTokenError(const QString &errorString); private: /** * @param parentObject The parent object. */ SessionManager(QObject *parentObject); SessionManager(const SessionManager&); // Not implemented. SessionManager& operator=(const SessionManager&); // Not implemented. UitapiService m_uit; /// Mutex to control access to session map. QMutex m_sessionsMutex; /// map of "username@realm" strings to UIT sessions. QMap m_sessions; }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* SESSIONMANAGER_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/sslsetup.cpp000066400000000000000000000041761323436134600230150ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "sslsetup.h" #include "logger.h" #include "molequeueconfig.h" #include #include #include #include #include namespace MoleQueue { namespace Uit { bool SslSetup::sslCertsLoaded = false; SslSetup::SslSetup() { } void SslSetup::init() { static QMutex mutex; if (!sslCertsLoaded) { QStringList certDirs; certDirs << QCoreApplication::applicationDirPath() + "/../" + MoleQueue_SSL_CERT_DIR; // for super build certDirs << QCoreApplication::applicationDirPath() + "/../molequeue/" + MoleQueue_SSL_CERT_DIR; QMutexLocker locker(&mutex); QSslConfiguration sslConf = QSslConfiguration::defaultConfiguration(); sslConf.setPeerVerifyMode(QSslSocket::VerifyNone); QSslConfiguration::setDefaultConfiguration(sslConf); if (!sslCertsLoaded) { foreach(const QString &dir, certDirs) { if (QDir(dir).exists()) { bool added = QSslSocket::addDefaultCaCertificates(dir + "/*", QSsl::Pem, QRegExp::Wildcard); if (!added) { Logger::logError(QObject::tr( "Error adding SSL certificates from %1") .arg(dir)); } } } sslCertsLoaded = true; } } } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/sslsetup.h000066400000000000000000000017121323436134600224530ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef SSLSETUP_H_ #define SSLSETUP_H_ namespace MoleQueue { namespace Uit { /** * @brief class used to initalize SSL certificates for QSslSocket. */ class SslSetup { private: SslSetup(); static bool sslCertsLoaded; public: static void init(); }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* SSLSETUP_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/userhostassoc.cpp000066400000000000000000000027611323436134600240360ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "userhostassoc.h" #include "messagehandler.h" #include #include namespace MoleQueue { namespace Uit { UserHostAssoc::UserHostAssoc() { } UserHostAssoc::UserHostAssoc(const UserHostAssoc &other) : m_hostID(other.hostId()), m_hostName(other.hostName()), m_systemName(other.systemName()), m_description(other.description()), m_account(other.account()), m_transportMethod(other.transportMethod()) { } UserHostAssoc &UserHostAssoc::operator=( const UserHostAssoc &other) { if (this != &other) { m_hostID = other.hostId(); m_hostName = other.hostName(); m_systemName = other.systemName(); m_description = other.description(); m_account = other.account(); m_transportMethod = other.transportMethod(); } return *this; } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/userhostassoc.h000066400000000000000000000052531323436134600235020ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef USERHOSTASSOC_H_ #define USERHOSTASSOC_H_ #include namespace MoleQueue { namespace Uit { /** * @brief class used to model UIT UserHostAssoc XML tag. */ class UserHostAssoc { public: UserHostAssoc(); /** * @param other The instance to copy. */ UserHostAssoc(const UserHostAssoc &other); /** * @param other The instance to assign. */ UserHostAssoc &operator=(const UserHostAssoc &other); /** * @return The account string. */ QString account() const { return m_account; } /** * @param acc The account string. */ void setAccount(const QString& acc) { m_account = acc; } /** * @return The description field. */ QString description() const { return m_description; } /** * @param des The description field. */ void setDescription(const QString& des) { m_description = des; } /** * @return host ID for the host associated with the user. */ qint64 hostId() const { return m_hostID; } /** * @param id The host ID for the host associated with this user. */ void setHostId(qint64 id) { m_hostID = id; } /** * @return The host name. */ QString hostName() const { return m_hostName; } /** * @param name The host name. */ void setHostName(const QString& name) { m_hostName = name; } /** * @return The system name. */ QString systemName() const { return m_systemName; } /** * @param name The system name. */ void setSystemName(const QString& name) { m_systemName = name; } /** * @return The transport method. */ QString transportMethod() const { return m_transportMethod; } /** * @param method The transport method. */ void setTransportMethod(const QString& method) { m_transportMethod = method; } private: qint64 m_hostID; QString m_hostName; QString m_systemName; QString m_description; QString m_account; QString m_transportMethod; }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* USERHOSTASSOC_H_ */ molequeue-0.9.0/molequeue/app/queues/uit/userhostassoclist.cpp000066400000000000000000000057621323436134600247360ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "userhostassoclist.h" #include "messagehandler.h" #include #include namespace MoleQueue { namespace Uit { UserHostAssocList::UserHostAssocList() : m_valid(false) { // TODO Auto-generated constructor stub } void UserHostAssocList::setContent(const QString &content) { m_xml = content; m_valid = true; MessageHandler handler; QXmlQuery query; query.setMessageHandler(&handler); m_valid = query.setFocus(m_xml); if (!m_valid) return; // First get the list of hostIDs query.setQuery("/list/PublicHostPlusUser/hostID/string()"); QStringList hostIDs; m_valid = query.evaluateTo(&hostIDs); if (!m_valid) return; QList assocs; foreach (const QString &id, hostIDs) { UserHostAssoc userHostAssoc; userHostAssoc.setHostId(id.toULongLong()); query.bindVariable("id", QVariant(id)); // Get the account QString user; query.setQuery("/list/PublicHostPlusUser[hostID=$id]/account/string()"); m_valid = query.evaluateTo(&user); if (!m_valid) return; userHostAssoc.setAccount(user.trimmed()); // Get the systemName QString system; query.setQuery("/list/PublicHostPlusUser[hostID=$id]/systemName/string()"); m_valid = query.evaluateTo(&system); if (!m_valid) return; userHostAssoc.setSystemName(system.trimmed()); // Get the transportMethod QString transport; query.setQuery( "/list/PublicHostPlusUser[hostID=$id]/transportMethod/string()"); m_valid = query.evaluateTo(&transport); if (!m_valid) return; userHostAssoc.setTransportMethod(transport.trimmed()); // Get the description QString des; query.setQuery("/list/PublicHostPlusUser[hostID=$id]/description/string()"); m_valid = query.evaluateTo(&des); if (!m_valid) return; userHostAssoc.setDescription(des.trimmed()); // Get the hostName QString host; query.setQuery("/list/PublicHostPlusUser[hostID=$id]/hostName/string()"); m_valid = query.evaluateTo(&host); if (!m_valid) return; userHostAssoc.setHostName(host.trimmed()); assocs.append(userHostAssoc); } m_userHostAssocs = assocs; } UserHostAssocList UserHostAssocList::fromXml(const QString &xml) { UserHostAssocList list; list.setContent(xml); return list; } } /* namespace Uit */ } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/app/queues/uit/userhostassoclist.h000066400000000000000000000033301323436134600243700ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef USERHOSTASSOCLIST_H_ #define USERHOSTASSOCLIST_H_ #include "userhostassoc.h" #include namespace MoleQueue { namespace Uit { /** * @brief class used to model a UIT UserHostAssocList XML document. */ class UserHostAssocList { public: /** * Static method to create a list UserHostAssoc instance from a incoming * XML message. * * @param The XML message. * @return The list of UserHostAssoc object */ static UserHostAssocList fromXml(const QString &xml); bool isValid() { return m_valid; } /** * @return The list of user host associations. */ QList userHostAssocs() const { return m_userHostAssocs; } /** * @return The raw XML used to generate this instance. */ QString xml() const { return m_xml; } private: bool m_valid; QList m_userHostAssocs; QString m_xml; UserHostAssocList(); /** * @param xml The XML to parse to populate this instance. */ void setContent(const QString &xml); }; } /* namespace Uit */ } /* namespace MoleQueue */ #endif /* USERHOSTASSOCLIST_H_ */ molequeue-0.9.0/molequeue/app/queuesettingsdialog.cpp000066400000000000000000000277001323436134600231060ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "queuesettingsdialog.h" #include "ui_queuesettingsdialog.h" #include "abstractqueuesettingswidget.h" #include "logger.h" #include "importprogramdialog.h" #include "queue.h" #include "queuemanager.h" #include "queueprogramitemmodel.h" #include "program.h" #include "programconfiguredialog.h" #include #include #include #include #include #include #include #include #include namespace MoleQueue { QueueSettingsDialog::QueueSettingsDialog(Queue *queue, QWidget *parentObject) : QDialog(parentObject), ui(new Ui::QueueSettingsDialog), m_queue(queue), m_model(new QueueProgramItemModel (m_queue, this)), m_settingsWidget(m_queue->settingsWidget()), m_dirty(true) { ui->setupUi(this); ui->nameLineEdit->setText(queue->name()); ui->typeNameLabel->setText(queue->typeName()); // add queue settings widget if (m_settingsWidget) { m_settingsWidget->setParent(ui->settingsFrame); ui->settingsLayout->addWidget(m_settingsWidget); m_settingsWidget->reset(); connect(m_settingsWidget, SIGNAL(modified()), SLOT(setDirty())); } // populate programs table ui->programsTable->setModel(m_model); ui->programsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); // Make connections connect(ui->addProgramButton, SIGNAL(clicked()), this, SLOT(addProgramClicked())); connect(ui->removeProgramButton, SIGNAL(clicked()), this, SLOT(removeProgramClicked())); connect(ui->configureProgramButton, SIGNAL(clicked()), this, SLOT(configureProgramClicked())); connect(ui->importProgramButton, SIGNAL(clicked()), this, SLOT(importProgramClicked())); connect(ui->exportProgramButton, SIGNAL(clicked()), this, SLOT(exportProgramClicked())); connect(ui->programsTable, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(doubleClicked(QModelIndex))); connect(ui->programsTable->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(enableProgramButtons(QItemSelection))); connect(ui->buttonBox, SIGNAL(clicked(QAbstractButton*)), this, SLOT(buttonBoxButtonClicked(QAbstractButton*))); connect(ui->nameLineEdit, SIGNAL(textChanged(QString)), SLOT(setDirty())); connect(ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(tabChanged(int))); ui->nameLineEdit->setValidator(new QRegExpValidator( QRegExp(VALID_NAME_REG_EXP))); setDirty(false); } QueueSettingsDialog::~QueueSettingsDialog() { delete ui; } void QueueSettingsDialog::accept() { if (!apply()) return; QDialog::accept(); } void QueueSettingsDialog::addProgramClicked() { Program *prog = new Program (m_queue); bool programAccepted = false; while (!programAccepted) { ProgramConfigureDialog configDialog(prog, this); DialogCode dialogCode = static_cast(configDialog.exec()); if (dialogCode == QDialog::Rejected) return; programAccepted = m_model->addProgram(prog); if (!programAccepted) { QMessageBox::information(this, tr("Cannot Add Program"), tr("Cannot add program: Another program with " "the same name exists. Please enter a " "different name.")); } } } void QueueSettingsDialog::removeProgramClicked() { QList selection = getSelectedPrograms(); foreach (Program *prog, selection) m_model->removeProgram(prog); setEnabledProgramButtons( !ui->programsTable->selectionModel()->selectedIndexes().isEmpty()); } void QueueSettingsDialog::configureProgramClicked() { QModelIndex index = ui->programsTable->currentIndex(); if (!index.isValid() || index.row() > m_queue->numPrograms()) return; showProgramConfigDialog(m_queue->programs().at(index.row())); } void QueueSettingsDialog::importProgramClicked() { ImportProgramDialog dialog(m_queue, this); dialog.exec(); } void QueueSettingsDialog::exportProgramClicked() { // Get selected Program QList selectedPrograms = this->getSelectedPrograms(); // Ensure that only one queue is selected at a time if (selectedPrograms.size() < 1) return; if (selectedPrograms.size() != 1) { QMessageBox::information(this, tr("Program Export"), tr("Please select only one program to export at a " "time."), QMessageBox::Ok); return; } Program *program= selectedPrograms.first(); // Get initial dir: QSettings settings; QString initialDir = settings.value("export/program/lastExportFile", QDir::homePath()).toString(); initialDir = QFileInfo(initialDir).dir().absolutePath() + QString("/%1-%2.mqp").arg(program->queueName(), program->name()); // Get filename for export QString exportFileName = QFileDialog::getSaveFileName(this, tr("Select export filename"), initialDir, tr("MoleQueue Program Export Format (*.mqp)" ";;All files (*)")); // User cancel: if (exportFileName.isNull()) return; // Set location for next time settings.setValue("export/program/lastExportFile", exportFileName); // Populate file program->exportSettings(exportFileName); } void QueueSettingsDialog::doubleClicked(const QModelIndex &index) { if (index.isValid() && index.row() <= m_queue->numPrograms()) showProgramConfigDialog(m_queue->programs().at(index.row())); } void QueueSettingsDialog::enableProgramButtons(const QItemSelection &selected) { setEnabledProgramButtons(!selected.isEmpty()); } QList QueueSettingsDialog::getSelectedRows() { QItemSelection sel (ui->programsTable->selectionModel()->selection()); QList rows; foreach (const QModelIndex &ind, sel.indexes()) { if (!rows.contains(ind.row())) rows << ind.row(); } qSort(rows); return rows; } QList QueueSettingsDialog::getSelectedPrograms() { QList allPrograms = m_queue->programs(); QList selectedPrograms; foreach (int i, getSelectedRows()) selectedPrograms << allPrograms.at(i); return selectedPrograms; } void QueueSettingsDialog::removeProgramDialog() { ProgramConfigureDialog *dialog = qobject_cast(sender()); if (!dialog) { Logger::logDebugMessage(tr("Internal error in %1: Sender is not a " "ProgramConfigureDialog (sender() = %2") .arg(Q_FUNC_INFO) .arg(sender() ? sender()->metaObject()->className() : "NULL")); return; } m_programConfigureDialogs.remove(dialog->currentProgram()); dialog->deleteLater(); } void QueueSettingsDialog::buttonBoxButtonClicked(QAbstractButton *button) { // "Ok" and "Cancel" are directly connected to accept() and reject(), so only // check for "apply" here: if (button == ui->buttonBox->button(QDialogButtonBox::Apply)) apply(); } bool QueueSettingsDialog::apply() { // If the name changed, check that it won't collide with an existing queue. QString name = ui->nameLineEdit->text().trimmed(); if (name != m_queue->name()) { if (QueueManager *queueManager = m_queue->queueManager()) { if (queueManager->queueNames().contains(name)) { int reply = QMessageBox::warning(this, tr("Name conflict"), tr("The queue name has been changed to '%1', " "but there is already a queue with that " "name.\n\nOverwrite existing queue?") .arg(name), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (reply != QMessageBox::Yes) { ui->nameLineEdit->selectAll(); ui->nameLineEdit->setFocus(); return false; } } m_queue->setName(name); } } if (m_settingsWidget && m_settingsWidget->isDirty()) m_settingsWidget->save(); setDirty(false); return true; } void QueueSettingsDialog::reset() { ui->nameLineEdit->setText(m_queue->name()); if (m_settingsWidget) m_settingsWidget->reset(); setDirty(false); } void QueueSettingsDialog::setDirty(bool dirty) { if (dirty != m_dirty) { m_dirty = dirty; ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(m_dirty); } } void QueueSettingsDialog::tabChanged(int index) { // We're only interested when the tab changes from settings to programs if (index == 0) return; // Does the configuration need to be saved? if (m_dirty) { // apply or discard changes? QMessageBox::StandardButton reply = QMessageBox::warning(this, tr("Unsaved changes"), tr("The changes to the queue have not been saved. " "Would you like to save or discard them?"), QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Save); switch (reply) { case QMessageBox::Cancel: ui->tabWidget->setCurrentIndex(0); return; case QMessageBox::Save: apply(); case QMessageBox::NoButton: case QMessageBox::Discard: default: reset(); break; } } } void QueueSettingsDialog::closeEvent(QCloseEvent *e) { if (m_dirty) { // apply or discard changes? QMessageBox::StandardButton reply = QMessageBox::warning(this, tr("Unsaved changes"), tr("The changes to the queue have not been saved. " "Would you like to save or discard them?"), QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Save); switch (reply) { case QMessageBox::Cancel: e->ignore(); return; case QMessageBox::Save: apply(); case QMessageBox::NoButton: case QMessageBox::Discard: default: reset(); e->accept(); break; } } QDialog::closeEvent(e); } void QueueSettingsDialog::keyPressEvent(QKeyEvent *e) { // By default, the escape key bypasses the close event, but we still want to // check if the settings widget is dirty. if (e->key() == Qt::Key_Escape) { e->accept(); close(); return; } QDialog::keyPressEvent(e); } void QueueSettingsDialog::showProgramConfigDialog(Program *prog) { ProgramConfigureDialog *dialog = NULL; // Check if there is already an open dialog for this queue dialog = m_programConfigureDialogs.value(prog, NULL); // If not, create one if (!dialog) { dialog = new ProgramConfigureDialog(prog, this); m_programConfigureDialogs.insert(prog, dialog); connect(dialog, SIGNAL(finished(int)), this, SLOT(removeProgramDialog())); } // Show and raise the dialog dialog->show(); dialog->raise(); } void QueueSettingsDialog::setEnabledProgramButtons(bool enabled) { ui->removeProgramButton->setEnabled(enabled); ui->configureProgramButton->setEnabled(enabled); ui->exportProgramButton->setEnabled(enabled); } } // end MoleQueue namespace molequeue-0.9.0/molequeue/app/queuesettingsdialog.h000066400000000000000000000045021323436134600225460ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_QUEUESETTINGSDIALOG_H #define MOLEQUEUE_QUEUESETTINGSDIALOG_H #include #include class QAbstractButton; class QItemSelection; class QModelIndex; namespace Ui { class QueueSettingsDialog; } namespace MoleQueue { class AbstractQueueSettingsWidget; class Program; class ProgramConfigureDialog; class Queue; class QueueProgramItemModel; /// @brief Dialog for configuring queues and managing programs. class QueueSettingsDialog : public QDialog { Q_OBJECT public: explicit QueueSettingsDialog(Queue *queue, QWidget *parentObject = 0); ~QueueSettingsDialog(); Queue *currentQueue() const { return m_queue; } public slots: void accept(); protected slots: void addProgramClicked(); void removeProgramClicked(); void configureProgramClicked(); void importProgramClicked(); void exportProgramClicked(); void doubleClicked(const QModelIndex &); void enableProgramButtons(const QItemSelection &selected); void showProgramConfigDialog(Program *prog); void setEnabledProgramButtons(bool enabled); void removeProgramDialog(); void buttonBoxButtonClicked(QAbstractButton*); bool apply(); void reset(); void setDirty(bool dirty = true); void tabChanged(int index); protected: void closeEvent(QCloseEvent *); void keyPressEvent(QKeyEvent *); /// Row indices, ascending order QList getSelectedRows(); QList getSelectedPrograms(); Ui::QueueSettingsDialog *ui; Queue *m_queue; QueueProgramItemModel *m_model; QMap m_programConfigureDialogs; AbstractQueueSettingsWidget *m_settingsWidget; bool m_dirty; }; } // end MoleQueue namespace #endif // QUEUESETTINGSDIALOG_H molequeue-0.9.0/molequeue/app/queuetray.qrc000066400000000000000000000002061323436134600210400ustar00rootroot00000000000000 icons/molequeue.png icons/MoleQueue_About.png molequeue-0.9.0/molequeue/app/remotequeuewidget.cpp000066400000000000000000000263251323436134600225670ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "remotequeuewidget.h" #include "ui_remotequeuewidget.h" #include #include #include "molequeueconfig.h" #include "program.h" #include "queues/remotessh.h" #include "sshcommandfactory.h" #include "templatekeyworddialog.h" #include #include #include #include #include #include namespace MoleQueue { RemoteQueueWidget::RemoteQueueWidget(QueueRemoteSsh *queue, QWidget *parentObject) : AbstractQueueSettingsWidget(parentObject), ui(new Ui::RemoteQueueWidget), m_queue(queue), m_client(NULL), m_helpDialog(NULL) { ui->setupUi(this); #ifndef MoleQueue_BUILD_CLIENT ui->push_sleepTest->hide(); #endif reset(); connect(ui->edit_submissionCommand, SIGNAL(textChanged(QString)), this, SLOT(setDirty())); connect(ui->edit_killCommand, SIGNAL(textChanged(QString)), this, SLOT(setDirty())); connect(ui->edit_requestQueueCommand, SIGNAL(textChanged(QString)), this, SLOT(setDirty())); connect(ui->updateIntervalSpin, SIGNAL(valueChanged(int)), this, SLOT(setDirty())); connect(ui->edit_launchScriptName, SIGNAL(textChanged(QString)), this, SLOT(setDirty())); connect(ui->edit_workingDirectoryBase, SIGNAL(textChanged(QString)), this, SLOT(setDirty())); connect(ui->sshExecutableEdit, SIGNAL(textChanged(QString)), this, SLOT(setDirty())); connect(ui->scpExecutableEdit, SIGNAL(textChanged(QString)), this, SLOT(setDirty())); connect(ui->editHostName, SIGNAL(textChanged(QString)), this, SLOT(setDirty())); connect(ui->editUserName, SIGNAL(textChanged(QString)), this, SLOT(setDirty())); connect(ui->editIdentityFile, SIGNAL(textChanged(QString)), this, SLOT(setDirty())); connect(ui->spinSshPort, SIGNAL(valueChanged(int)), this, SLOT(setDirty())); connect(ui->text_launchTemplate, SIGNAL(textChanged()), this, SLOT(setDirty())); connect(ui->wallTimeHours, SIGNAL(valueChanged(int)), this, SLOT(setDirty())); connect(ui->wallTimeMinutes, SIGNAL(valueChanged(int)), this, SLOT(setDirty())); connect(ui->pushTestConnection, SIGNAL(clicked()), this, SLOT(testConnection())); connect(ui->push_sleepTest, SIGNAL(clicked()), this, SLOT(sleepTest())); connect(ui->templateHelpButton, SIGNAL(clicked()), this, SLOT(showHelpDialog())); connect(ui->fileButton, SIGNAL(clicked()), this, SLOT(showFileDialog())); } RemoteQueueWidget::~RemoteQueueWidget() { delete ui; } void RemoteQueueWidget::save() { m_queue->setSubmissionCommand(ui->edit_submissionCommand->text()); m_queue->setKillCommand(ui->edit_killCommand->text()); m_queue->setRequestQueueCommand(ui->edit_requestQueueCommand->text()); m_queue->setLaunchScriptName(ui->edit_launchScriptName->text()); m_queue->setWorkingDirectoryBase(ui->edit_workingDirectoryBase->text()); m_queue->setSshExecutable(ui->sshExecutableEdit->text()); m_queue->setScpExecutable(ui->scpExecutableEdit->text()); m_queue->setHostName(ui->editHostName->text()); m_queue->setUserName(ui->editUserName->text()); m_queue->setIdentityFile(ui->editIdentityFile->text()); m_queue->setSshPort(ui->spinSshPort->value()); m_queue->setQueueUpdateInterval(ui->updateIntervalSpin->value()); QString text = ui->text_launchTemplate->document()->toPlainText(); m_queue->setLaunchTemplate(text); int hours = ui->wallTimeHours->value(); int minutes = ui->wallTimeMinutes->value(); m_queue->setDefaultMaxWallTime(minutes + (hours * 60)); setDirty(false); } void RemoteQueueWidget::reset() { ui->edit_submissionCommand->setText(m_queue->submissionCommand()); ui->edit_killCommand->setText(m_queue->killCommand()); ui->edit_requestQueueCommand->setText(m_queue->requestQueueCommand()); ui->edit_launchScriptName->setText(m_queue->launchScriptName()); ui->edit_workingDirectoryBase->setText(m_queue->workingDirectoryBase()); ui->updateIntervalSpin->setValue(m_queue->queueUpdateInterval()); int walltime = m_queue->defaultMaxWallTime(); ui->wallTimeHours->setValue(walltime / 60); ui->wallTimeMinutes->setValue(walltime % 60); ui->sshExecutableEdit->setText(m_queue->sshExecutable()); ui->scpExecutableEdit->setText(m_queue->scpExectuable()); ui->editHostName->setText(m_queue->hostName()); ui->editUserName->setText(m_queue->userName()); ui->editIdentityFile->setText(m_queue->identityFile()); ui->spinSshPort->setValue(m_queue->sshPort()); ui->text_launchTemplate->document()->setPlainText(m_queue->launchTemplate()); setDirty(false); } void RemoteQueueWidget::testConnection() { // Verify information QString sshCommand = ui->sshExecutableEdit->text(); QString host = ui->editHostName->text(); QString user = ui->editUserName->text(); QString identityFile = ui->editIdentityFile->text(); int port = ui->spinSshPort->value(); if (host.isEmpty() || user.isEmpty()) { QMessageBox::warning(this, tr("Cannot connect to remote host."), tr("Cannot connect to remote host: invalid host " "specification: %1@%2").arg(host, user)); return; } // Create SSH connection SshCommand *conn = SshCommandFactory::instance()->newSshCommand(); conn->setSshCommand(sshCommand); conn->setHostName(host); conn->setUserName(user); conn->setIdentityFile(identityFile); conn->setPortNumber(port); // Create ProgressDialog QProgressDialog *prog = new QProgressDialog (this); prog->setWindowTitle(tr("Testing remote connection...")); prog->setLabelText(tr("Attempting to connect to %1@%2:%3...") .arg(user).arg(host).arg(port)); prog->setMinimumDuration(0); prog->setWindowModality(Qt::WindowModal); prog->setRange(0, 0); prog->setValue(0); QTimer *timeout = new QTimer (this); connect(conn, SIGNAL(requestComplete()), prog, SLOT(accept())); connect(timeout, SIGNAL(timeout()), prog, SLOT(reject())); // Wait 15 seconds for timeout timeout->start(15000); conn->execute("echo ok"); prog->exec(); prog->hide(); if (prog->wasCanceled()) { conn->deleteLater(); prog->deleteLater(); return; } if (prog->result() == QProgressDialog::Rejected) { QMessageBox::critical(this, tr("Connection timeout"), tr("The connection to %1@%2:%3 failed: connection" " timed out.").arg(user).arg(host).arg(port)); conn->deleteLater(); prog->deleteLater(); return; } prog->hide(); prog->deleteLater(); // Verify output and exit code if (conn->exitCode() != 0 || conn->output().trimmed() != "ok") { QMessageBox::critical(this, tr("SSH Error"), tr("The connection to %1@%2:%3 failed: " "exit code: %4. Output:\n\n%5") .arg(user).arg(host).arg(port) .arg(conn->exitCode()).arg(conn->output())); conn->deleteLater(); return; } QMessageBox::information(this, tr("Success"), tr("SSH connection to %1@%2:%3 succeeded!") .arg(user).arg(host).arg(port)); conn->deleteLater(); return; } void RemoteQueueWidget::sleepTest() { #ifdef MoleQueue_BUILD_CLIENT QString promptString; if (isDirty()) { promptString = tr("Would you like to apply the current settings and submit " "a test job? The job will run 'sleep 30' on the remote " "queue."); } else { promptString = tr("Would you like to submit a test job? The job will run " "'sleep 30' on the remote queue."); } QMessageBox::StandardButton response = QMessageBox::question(this, tr("Submit test job?"), promptString, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); if (response != QMessageBox::Yes) return; if (isDirty()) save(); // Check that important variables are set: QString missingVariable = ""; if (m_queue->hostName().isEmpty()) missingVariable = tr("server hostname"); else if (m_queue->userName().isEmpty()) missingVariable = tr("server username"); else if (m_queue->submissionCommand().isEmpty()) missingVariable = tr("job submission command"); else if (m_queue->killCommand().isEmpty()) missingVariable = tr("job cancel command"); else if (m_queue->requestQueueCommand().isEmpty()) missingVariable = tr("queue request command"); else if (m_queue->workingDirectoryBase().isEmpty()) missingVariable = tr("remote working directory"); if (!missingVariable.isEmpty()) { QMessageBox::critical(this, tr("Missing information"), tr("Refusing to test job submission: %1 not set.") .arg(missingVariable)); return; } Program *sleepProgram = m_queue->lookupProgram("sleep (testing)"); if (sleepProgram == NULL) { // Add sleep if it's not present sleepProgram = new Program (m_queue); sleepProgram->setName("sleep (testing)"); sleepProgram->setArguments("30"); sleepProgram->setExecutable("sleep"); sleepProgram->setOutputFilename(""); sleepProgram->setLaunchSyntax(Program::PLAIN); m_queue->addProgram(sleepProgram); } if (!m_client) { m_client = new Client (this); m_client->connectToServer(); } JobObject sleepJob; sleepJob.setQueue(m_queue->name()); sleepJob.setProgram(sleepProgram->name()); sleepJob.setDescription("sleep 30 (test)"); m_client->submitJob(sleepJob); #endif // MoleQueue_BUILD_CLIENT } void RemoteQueueWidget::showHelpDialog() { if (!m_helpDialog) m_helpDialog = new TemplateKeywordDialog(this); m_helpDialog->show(); } void RemoteQueueWidget::showFileDialog() { // Get initial dir: QSettings settings; QString initialDir = settings.value("ssh/identity/lastIdentityFile", ui->editIdentityFile->text()).toString(); if (initialDir.isEmpty()) { initialDir = QDir::homePath(); #ifndef _WIN32 initialDir += "/.ssh/"; #endif } initialDir = QFileInfo(initialDir).dir().absolutePath(); // Get filename QString identityFileName = QFileDialog::getOpenFileName(this, tr("Select identity file"), initialDir); // User cancel: if (identityFileName.isNull()) return; // Set location for next time settings.setValue("ssh/identity/lastIdentityFile", identityFileName); ui->editIdentityFile->setText(identityFileName); } } // end namespace MoleQueue molequeue-0.9.0/molequeue/app/remotequeuewidget.h000066400000000000000000000031541323436134600222270ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef REMOTEQUEUEWIDGET_H #define REMOTEQUEUEWIDGET_H #include "abstractqueuesettingswidget.h" namespace Ui { class RemoteQueueWidget; } namespace MoleQueue { class Client; class QueueRemoteSsh; class TemplateKeywordDialog; /** * @class RemoteQueueWidget remotequeuewidget.h * * @brief A generic configuration dialog for remote queuing systems. * * @author David C. Lonie */ class RemoteQueueWidget: public AbstractQueueSettingsWidget { Q_OBJECT public: explicit RemoteQueueWidget(QueueRemoteSsh *queue, QWidget *parentObject = 0); ~RemoteQueueWidget(); public slots: void save(); void reset(); protected slots: void testConnection(); void sleepTest(); void showHelpDialog(); private slots: void showFileDialog(); private: Ui::RemoteQueueWidget *ui; QueueRemoteSsh *m_queue; Client *m_client; // Used for submitting test jobs. TemplateKeywordDialog *m_helpDialog; }; } // end namespace MoleQueue #endif // REMOTEQUEUEWIDGET_H molequeue-0.9.0/molequeue/app/server.cpp000066400000000000000000000622671323436134600203360ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "server.h" #include "actionfactorymanager.h" #include "job.h" #include "jobmanager.h" #include "logger.h" #include "queue.h" #include "queuemanager.h" #include "pluginmanager.h" #include "jobactionfactories/openwithactionfactory.h" #include #include #include #include #include #include #include #include #include #include namespace MoleQueue { Server::Server(QObject *parentObject, QString serverName_) : QObject(parentObject), m_jobManager(new JobManager (this)), m_queueManager(new QueueManager (this)), m_jsonrpc(new JsonRpc(this)), m_moleQueueIdCounter(0), m_serverName(serverName_), m_jobSyncTimer(startTimer(20000)) // 20 seconds { qRegisterMetaType("ConnectionListener::Error"); qRegisterMetaType("const MoleQueue::Job*"); qRegisterMetaType("MoleQueue::QueueListType"); connect(m_jsonrpc, SIGNAL(messageReceived(MoleQueue::Message)), SLOT(handleMessage(MoleQueue::Message))); connect(m_jobManager, SIGNAL(jobAboutToBeAdded(MoleQueue::Job)), this, SLOT(jobAboutToBeAdded(MoleQueue::Job)), Qt::DirectConnection); connect(m_jobManager, SIGNAL(jobStateChanged(const MoleQueue::Job&, MoleQueue::JobState, MoleQueue::JobState)), this, SLOT(dispatchJobStateChange(const MoleQueue::Job&, MoleQueue::JobState, MoleQueue::JobState))); connect(m_jobManager, SIGNAL(jobRemoved(MoleQueue::IdType)), this, SLOT(jobRemoved(MoleQueue::IdType))); // load the transport plugins so we know what to listen on PluginManager *pluginManager = PluginManager::instance(); pluginManager->load(); } Server::~Server() { if (m_jobSyncTimer != 0) killTimer(m_jobSyncTimer); stop(); delete m_jobManager; m_jobManager = NULL; delete m_queueManager; m_queueManager = NULL; } void Server::createConnectionListeners() { PluginManager *pluginManager = PluginManager::instance(); QList factories = pluginManager->connectionListenerFactories(); foreach(ConnectionListenerFactory *factory, factories) { ConnectionListener *listener = factory->createConnectionListener(this, m_serverName); connect(listener, SIGNAL(connectionError(MoleQueue::ConnectionListener::Error, const QString&)), this, SIGNAL(connectionError(MoleQueue::ConnectionListener::Error, const QString&))); connect(listener, SIGNAL(newConnection(MoleQueue::Connection*)), this, SLOT(newConnectionAvailable(MoleQueue::Connection*))); m_connectionListeners.append(listener); m_jsonrpc->addConnectionListener(listener); } } void Server::readSettings(QSettings &settings) { m_workingDirectoryBase = settings.value( "workingDirectoryBase", QDir::homePath() + "/.molequeue/local").toString(); QDir dir; dir.mkpath(m_workingDirectoryBase); m_moleQueueIdCounter = settings.value("moleQueueIdCounter", 0).value(); m_queueManager->readSettings(); const QString jobsDir = m_workingDirectoryBase + "/jobs"; dir.mkpath(jobsDir); m_jobManager->loadJobState(jobsDir); } void Server::writeSettings(QSettings &settings) const { settings.setValue("workingDirectoryBase", m_workingDirectoryBase); settings.setValue("moleQueueIdCounter", m_moleQueueIdCounter); m_queueManager->writeSettings(); m_jobManager->syncJobState(); } void Server::start() { if(m_connectionListeners.empty()) createConnectionListeners(); foreach (ConnectionListener *listener, m_connectionListeners) { listener->start(); } Logger::logDebugMessage(tr("Server started listening on address '%1'") .arg(m_serverName)); } void Server::forceStart() { // Force stop and restart stop(true); start(); } void Server::stop(bool force) { foreach (Connection *conn, m_connections) { conn->close(); delete conn; } foreach (ConnectionListener *listener, m_connectionListeners) { listener->stop(force); delete listener; } m_connections.clear(); m_connectionListeners.clear(); } void Server::stop() { stop(false); } void Server::dispatchJobStateChange(const Job &job, JobState oldState, JobState newState) { Connection *connection = m_connectionLUT.value(job.moleQueueId()); EndpointIdType endpoint = m_endpointLUT.value(job.moleQueueId()); if (connection == NULL) return; Message msg(Message::Notification, connection, endpoint); msg.setMethod("jobStateChanged"); QJsonObject paramsObject; paramsObject.insert("moleQueueId", idTypeToJson(job.moleQueueId())); paramsObject.insert("oldState", QString(jobStateToString(oldState))); paramsObject.insert("newState", QString(jobStateToString(newState))); msg.setParams(paramsObject); msg.send(); } void Server::jobAboutToBeAdded(Job job) { IdType nextMoleQueueId = ++m_moleQueueIdCounter; QSettings settings; settings.setValue("moleQueueIdCounter", m_moleQueueIdCounter); job.setMoleQueueId(nextMoleQueueId); job.setLocalWorkingDirectory(m_workingDirectoryBase + "/jobs/" + idTypeToString(nextMoleQueueId)); // If the outputDirectory is blank, set it now if (job.outputDirectory().isEmpty()) job.setOutputDirectory(job.localWorkingDirectory()); // Create the local working directory if (job.localWorkingDirectory().isEmpty() || !QDir().mkpath(job.localWorkingDirectory())) { Logger::logError(tr("Error creating working directory for job %1 " "(dir='%2')").arg(idTypeToString(job.moleQueueId())) .arg(job.localWorkingDirectory()), job.moleQueueId()); } } void Server::newConnectionAvailable(Connection *connection) { m_connections.append(connection); connect(connection, SIGNAL(disconnected()), this, SLOT(clientDisconnected())); Logger::logDebugMessage(tr("Client connected: %1") .arg(connection->connectionString())); } void Server::clientDisconnected() { Connection *conn = qobject_cast(sender()); if (conn == NULL) return; Logger::logDebugMessage(tr("Client disconnected: %1") .arg(conn->connectionString())); m_connections.removeOne(conn); // Remove connection from look up table and any endpoints key on molequeueids // associated with that connection. QList moleQueueIds = m_connectionLUT.keys(conn); foreach(IdType moleQueueId, moleQueueIds) { m_connectionLUT.remove(moleQueueId); m_endpointLUT.remove(moleQueueId); } conn->deleteLater(); } void Server::jobRemoved(MoleQueue::IdType moleQueueId) { m_connectionLUT.remove(moleQueueId); m_endpointLUT.remove(moleQueueId); } void Server::handleMessage(const Message &message) { switch (message.type()) { case Message::Request: handleRequest(message); break; default: Logger::logDebugMessage(tr("Unhandled message; no handler for type: %1\n%2") .arg(message.type()) .arg(QString(message.toJson()))); break; } } void Server::handleRequest(const Message &message) { const QString method = message.method(); if (method == "listQueues") handleListQueuesRequest(message); else if (method == "submitJob") handleSubmitJobRequest(message); else if (method == "cancelJob") handleCancelJobRequest(message); else if (method == "lookupJob") handleLookupJobRequest(message); else if (method == "registerOpenWith") handleRegisterOpenWithRequest(message); else if (method == "listOpenWithNames") handleListOpenWithNamesRequest(message); else if (method == "unregisterOpenWith") handleUnregisterOpenWithRequest(message); else if (method == "rpcKill") handleRpcKillRequest(message); else handleUnknownMethod(message); } void Server::handleUnknownMethod(const Message &message) { Message errorMessage = message.generateErrorResponse(); errorMessage.setErrorCode(-32601); errorMessage.setErrorMessage("Method not found"); QJsonObject errorDataObject; errorDataObject.insert("request", message.toJsonObject()); errorMessage.setErrorData(errorDataObject); errorMessage.send(); Logger::logDebugMessage( tr("Received JSON-RPC request with invalid method '%1':\n%2") .arg(message.method()).arg(QString(message.toJson()))); } void Server::handleInvalidParams(const Message &message, const QString &description) { Message errorMessage = message.generateErrorResponse(); errorMessage.setErrorCode(-32602); errorMessage.setErrorMessage("Invalid params"); QJsonObject errorDataObject; errorDataObject.insert("description", description); errorDataObject.insert("request", message.toJsonObject()); errorMessage.setErrorData(errorDataObject); errorMessage.send(); Logger::logDebugMessage( tr("Received JSON-RPC request with invalid parameters (%1):\n%2") .arg(description).arg(QString(message.toJson()))); } void Server::handleListQueuesRequest(const Message &message) { // Build result object (queue list) QueueListType queueList = m_queueManager->toQueueList(); QJsonObject jsonQueueList; foreach (QString queueName, queueList.keys()) { jsonQueueList.insert(queueName, QJsonArray::fromStringList(queueList[queueName])); } // Create response message Message response = message.generateResponse(); response.setResult(jsonQueueList); response.send(); } void Server::handleSubmitJobRequest(const Message &message) { // Validate params -- are the params an object? if (!message.params().isObject()) { handleInvalidParams(message, "submitJob params member must be an object."); return; } QJsonObject paramsObject = message.params().toObject(); // Are the required queue and program members present? if (!paramsObject.contains("queue")) { handleInvalidParams(message, "Required params.queue member missing."); return; } if (!paramsObject.contains("program")) { handleInvalidParams(message, "Required params.program member missing."); return; } if (!paramsObject.value("queue").isString()) { handleInvalidParams(message, "params.queue member must be a string."); return; } if (!paramsObject.value("program").isString()) { handleInvalidParams(message, "params.program member must be a string."); return; } // Do the queue and program exist? QString queueString = paramsObject.value("queue").toString(); QString programString = paramsObject.value("program").toString(); Queue *queue = m_queueManager->lookupQueue(queueString); if (!queue) { Message errorMessage = message.generateErrorResponse(); errorMessage.setErrorCode(MoleQueue::InvalidQueue); errorMessage.setErrorMessage("Invalid queue"); QJsonObject errorDataObject; errorDataObject.insert("queue", queueString); QJsonArray validQueues = QJsonArray::fromStringList(m_queueManager->queueNames()); errorDataObject.insert("valid queues", validQueues); errorDataObject.insert("request", message.toJsonObject()); errorMessage.setErrorData(errorDataObject); errorMessage.send(); Logger::logDebugMessage( tr("Received submitJob request with invalid queue (%1):\n%2") .arg(queueString).arg(QString(message.toJson()))); return; } Program *program = queue->lookupProgram(programString); if (!program) { Message errorMessage = message.generateErrorResponse(); errorMessage.setErrorCode(MoleQueue::InvalidProgram); errorMessage.setErrorMessage("Invalid program"); QJsonObject errorDataObject; errorDataObject.insert("program", programString); QJsonArray validPrograms = QJsonArray::fromStringList(queue->programNames()); errorDataObject.insert("valid programs for queue", validPrograms); errorDataObject.insert("request", message.toJsonObject()); errorMessage.setErrorData(errorDataObject); errorMessage.send(); Logger::logDebugMessage( tr("Received submitJob request with invalid program (%1/%2):\n%3") .arg(queueString).arg(programString).arg(QString(message.toJson()))); return; } // Everything checks out -- Create the job and send the response. Job job = m_jobManager->newJob(paramsObject); Logger::logDebugMessage(tr("Job submission requested:\n%1") .arg(QString(message.toJson())), job.moleQueueId()); Message response = message.generateResponse(); QJsonObject resultObject; resultObject.insert("moleQueueId", idTypeToJson(job.moleQueueId())); resultObject.insert("workingDirectory", job.localWorkingDirectory()); response.setResult(resultObject); response.send(); m_connectionLUT.insert(job.moleQueueId(), message.connection()); m_endpointLUT.insert(job.moleQueueId(), message.endpoint()); // Submit the job after sending the response -- otherwise the client can // receive job state change notifications for a job before knowing its // MoleQueueId... queue->submitJob(job); } void Server::handleCancelJobRequest(const Message &message) { // Validate request if (!message.params().isObject()) { handleInvalidParams(message, "cancelJob params member must be an object."); return; } QJsonObject paramsObject = message.params().toObject(); // Is the required moleQueueId member present? if (!paramsObject.contains("moleQueueId")) { handleInvalidParams(message, "Required params.moleQueueId member missing."); return; } // Is the required moleQueueId member valid? IdType moleQueueId = toIdType(paramsObject.value("moleQueueId")); Job job = m_jobManager->lookupJobByMoleQueueId(moleQueueId); if (!job.isValid()) { Message errorMessage = message.generateErrorResponse(); errorMessage.setErrorCode(MoleQueue::InvalidMoleQueueId); errorMessage.setErrorMessage("Unknown MoleQueue ID"); QJsonObject errorDataObject; errorDataObject.insert("moleQueueId", paramsObject.value("moleQueueId")); errorMessage.setErrorData(errorDataObject); errorMessage.send(); Logger::logDebugMessage( tr("Received cancelJob request with invalid MoleQueue ID (%1):\n%2") .arg(idTypeToString(moleQueueId)).arg(QString(message.toJson())), moleQueueId); return; } // Is the job in a state that it can be canceled? JobState state = job.jobState(); bool stateValid = false; switch (state) { case MoleQueue::Accepted: case MoleQueue::QueuedLocal: case MoleQueue::Submitted: case MoleQueue::QueuedRemote: case MoleQueue::RunningLocal: case MoleQueue::RunningRemote: stateValid = true; default: break; } if (!stateValid) { Message errorMessage = message.generateErrorResponse(); errorMessage.setErrorCode(MoleQueue::InvalidJobState); errorMessage.setErrorMessage("Cannot cancel job: Job not running."); QJsonObject errorDataObject; errorDataObject.insert("moleQueueId", paramsObject.value("moleQueueId")); errorDataObject.insert("jobState", QLatin1String(jobStateToString(state))); errorMessage.setErrorData(errorDataObject); errorMessage.send(); Logger::logDebugMessage( tr("Received cancelJob request for non-running job (%1, %2):\n%3") .arg(idTypeToString(moleQueueId)) .arg(jobStateToGuiString(state)) .arg(QString(message.toJson())), moleQueueId); return; } Queue *queue = m_queueManager->lookupQueue(job.queue()); if (!queue) { Message errorMessage = message.generateErrorResponse(); errorMessage.setErrorCode(MoleQueue::InvalidQueue); errorMessage.setErrorMessage("Queue no longer exists"); QJsonObject errorDataObject; errorDataObject.insert("moleQueueId", paramsObject.value("moleQueueId")); errorDataObject.insert("queue", job.queue()); errorMessage.setErrorData(errorDataObject); errorMessage.send(); Logger::logDebugMessage( tr("Received cancelJob request for deleted queue (%1, %2):\n%3") .arg(idTypeToString(moleQueueId)) .arg(job.queue()) .arg(QString(message.toJson()))); return; } queue->killJob(job); Message response = message.generateResponse(); QJsonObject resultObject; resultObject.insert("moleQueueId", idTypeToJson(moleQueueId)); response.setResult(resultObject); response.send(); } void Server::handleLookupJobRequest(const Message &message) { // Validate request if (!message.params().isObject()) { handleInvalidParams(message, "lookupJob params member must be an object."); return; } QJsonObject paramsObject = message.params().toObject(); // Is the required moleQueueId member present? if (!paramsObject.contains("moleQueueId")) { handleInvalidParams(message, "Required params.moleQueueId member missing."); return; } // Is the required moleQueueId member valid? IdType moleQueueId = toIdType(paramsObject.value("moleQueueId")); Job job = m_jobManager->lookupJobByMoleQueueId(moleQueueId); if (!job.isValid()) { Message errorMessage = message.generateErrorResponse(); errorMessage.setErrorCode(MoleQueue::InvalidMoleQueueId); errorMessage.setErrorMessage("Unknown MoleQueue ID"); QJsonObject errorDataObject; errorDataObject.insert("moleQueueId", paramsObject.value("moleQueueId")); errorMessage.setErrorData(errorDataObject); errorMessage.send(); Logger::logDebugMessage( tr("Received lookupJob request with invalid MoleQueue ID (%1):\n%2") .arg(idTypeToString(moleQueueId)).arg(QString(message.toJson())), moleQueueId); return; } // Send reply Message response = message.generateResponse(); response.setResult(job.toJsonObject()); response.send(); } void Server::handleRegisterOpenWithRequest(const Message &message) { // validate request if (!message.params().isObject()) { handleInvalidParams(message, "registerOpenWith params member must be an object."); return; } QJsonObject paramsObject = message.params().toObject(); // At a minimum, name and method must be specified: if (!paramsObject["name"].isString() || !paramsObject["method"].isObject()) { handleInvalidParams(message, "\"params.name\" (string) and " "\"params.method\" (object) must both be present."); return; } const QString name(paramsObject["name"].toString()); const QJsonObject methodObject(paramsObject["method"].toObject()); OpenWithActionFactory::HandlerType handlerType; QString executable; QString rpcServer; QString rpcMethod; if (methodObject["executable"].isString()) { handlerType = OpenWithActionFactory::ExecutableHandler; executable = methodObject["executable"].toString(); } else if (methodObject["rpcServer"].isString() && methodObject["rpcMethod"].isString()) { handlerType = OpenWithActionFactory::RpcHandler; rpcServer = methodObject["rpcServer"].toString(); rpcMethod = methodObject["rpcMethod"].toString(); } else { handleInvalidParams(message, "\"params.method\" invalid."); return; } if (name.isEmpty()) { handleInvalidParams(message, "\"params.name\" must be a non-empty string."); return; } // Validate and extract patterns. QList patterns; if (paramsObject.contains("patterns")) { if (!paramsObject["patterns"].isArray()) { handleInvalidParams(message, "\"params.patterns\" member must be a JSON " "array."); return; } foreach (const QJsonValue &pattern, paramsObject["patterns"].toArray()) { if (!pattern.isObject()) { handleInvalidParams(message, "\"params.patterns\" array entries must " "be JSON objects."); return; } const QJsonObject patternObject = pattern.toObject(); QRegExp regexp; if (patternObject["regexp"].isString()) { regexp.setPatternSyntax(QRegExp::RegExp2); regexp.setPattern(patternObject["regexp"].toString()); } else if (patternObject["wildcard"].isString()) { regexp.setPatternSyntax(QRegExp::WildcardUnix); regexp.setPattern(patternObject["wildcard"].toString()); } else { handleInvalidParams(message, "\"params.patterns\" contains an entry " "that is not a regexp or wildcard."); return; } if (patternObject.contains("caseSensitive")) { bool caseSensitive(patternObject.value("caseSensitive").toBool(true)); regexp.setCaseSensitivity(caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive); } patterns << regexp; } } // If no patterns are specified, match all files: if (patterns.empty()) patterns << QRegExp("*", Qt::CaseSensitive, QRegExp::WildcardUnix); // Get existing open-with handlers ActionFactoryManager *afm = ActionFactoryManager::instance(); QList factories = afm->factoriesOfType(); // Check for name conflicts: foreach (const OpenWithActionFactory *factory, factories) { if (factory->name() == name) { Message error = message.generateErrorResponse(); error.setErrorCode(1); error.setErrorMessage( QLatin1Literal("Name conflict: An open-with handler named '") % name % QLatin1Literal("' already exists.")); error.send(); return; } } // Create a new handler: OpenWithActionFactory *newFactory(new OpenWithActionFactory); newFactory->setName(name); newFactory->setFilePatterns(patterns); switch (handlerType) { case OpenWithActionFactory::ExecutableHandler: newFactory->setExecutable(executable); break; case OpenWithActionFactory::RpcHandler: newFactory->setRpcDetails(rpcServer, rpcMethod); break; default: case OpenWithActionFactory::NoHandler: break; } afm->addFactory(newFactory); Message response = message.generateResponse(); response.setResult(QLatin1String("success")); response.send(); } void Server::handleListOpenWithNamesRequest(const Message &message) { // Build result object ActionFactoryManager *afm = ActionFactoryManager::instance(); QList handlers( afm->factoriesOfType()); QJsonArray result; foreach (OpenWithActionFactory *handler, handlers) result.append(handler->name()); // Create response message Message response = message.generateResponse(); response.setResult(result); response.send(); } void Server::handleUnregisterOpenWithRequest(const Message &message) { // Validate if (!message.params().isObject()) { handleInvalidParams(message, "params value must be an object."); return; } QJsonObject paramsObject(message.params().toObject()); if (!paramsObject["name"].isString()) { handleInvalidParams(message, "\"params.name\" value must be a string."); return; } QString handlerName(paramsObject["name"].toString()); // Search for matching handler ActionFactoryManager *afm = ActionFactoryManager::instance(); QList handlers = afm->factoriesOfType(); OpenWithActionFactory *handler; foreach (OpenWithActionFactory *h, handlers) { if (handlerName == h->name()) { handler = h; break; } } if (!handler) { Message error = message.generateErrorResponse(); error.setErrorCode(1); error.setErrorMessage(QString("File handler '%1'' not found!") .arg(handlerName)); error.send(); return; } // Remove handler afm->removeFactory(handler); // Send response Message response = message.generateResponse(); response.setResult(QLatin1String("success")); response.send(); } void Server::handleRpcKillRequest(const Message &message) { QSettings settings; bool enabled = settings.value("enableRpcKill", false).toBool(); Message response = message.generateResponse(); QJsonObject resultObject; resultObject.insert("success", enabled); response.setResult(resultObject); response.send(); if (enabled) { qApp->processEvents(QEventLoop::AllEvents, 1000); qApp->quit(); } } void Server::timerEvent(QTimerEvent *e) { if (e->timerId() == m_jobSyncTimer) { e->accept(); m_jobManager->syncJobState(); return; } QObject::timerEvent(e); } } // end namespace MoleQueue molequeue-0.9.0/molequeue/app/server.h000066400000000000000000000145531323436134600177760ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_SERVER_H #define MOLEQUEUE_SERVER_H #include #include "job.h" #include #include #include class ServerTest; class QSettings; namespace MoleQueue { class Connection; class Job; class JobManager; class QueueManager; class JsonRpc; /** * @brief The Server class handles incoming JSON-RPC messages. * @author David C. Lonie * * The Server class is the root of the server-side heirarchy. It owns the * JobManager, QueueManager, and JsonRpc listener. */ class Server : public QObject { Q_OBJECT public: /** * Constructor. * * @param parentObject The parent. */ explicit Server(QObject *parentObject = 0, QString serverName_ = "MoleQueue"); /** * Destructor. */ ~Server(); /** * @return A pointer to the Server JobManager. */ JobManager *jobManager() {return m_jobManager;} /** * @return A pointer to the Server JobManager. */ const JobManager *jobManager() const {return m_jobManager;} /** * @return A pointer to the Server QueueManager. */ QueueManager *queueManager() {return m_queueManager;} /** * @return A pointer to the Server QueueManager. */ const QueueManager *queueManager() const {return m_queueManager;} /// @param settings QSettings object to write state to. void readSettings(QSettings &settings); /// @param settings QSettings object to read state from. void writeSettings(QSettings &settings) const; /// The working directory where running job file are kept. QString workingDirectoryBase() const {return m_workingDirectoryBase;} /// The string the server uses to listen for connections. QString serverName() const { return m_serverName; } /// Used for unit testing friend class ::ServerTest; signals: /** * Emitted when a connection listener fails to start. */ void connectionError(MoleQueue::ConnectionListener::Error error, const QString &message); public slots: /** * Start listening for incoming connections. * * If an error occurs, connectionError will be emitted. If an * AddressInUseError occurs on Unix due to a crashed Server that failed to * clean up, call forceStart to remove any existing sockets. */ void start(); /** * Start listening for incoming connections, removing any existing socket * handles first. */ void forceStart(); /** * Terminate the socket server. * * @param Server will pass the value of force when stop it connections. */ void stop(bool force); /** * Terminate the socket server. * * Same as stop(false) */ void stop(); /** * Find the client that owns @a job and send a notification to the client that * the JobState has changed. * @param job Job of interest. * @param oldState Previous state of @a job. * @param newState New state of @a job. */ void dispatchJobStateChange(const MoleQueue::Job &job, MoleQueue::JobState oldState, MoleQueue::JobState newState); protected slots: /** * Set the MoleQueue Id of a job before it is added to the manager. * @param job The new Job. */ void jobAboutToBeAdded(MoleQueue::Job job); /** * Called when the internal socket server has a new connection ready. */ void newConnectionAvailable(MoleQueue::Connection *connection); /** * Called when a client disconnects from the server. This function expects * sender() to return a ServerConnection. */ void clientDisconnected(); /** * @brief handleMessage Called when the JsonRpc listener receives a message. */ void handleMessage(const MoleQueue::Message &message); private slots: /** * Called to clean up connection map when a job is removed ... */ void jobRemoved(MoleQueue::IdType moleQueueId); private: /** * @{ * Handlers for different message types. */ void handleRequest(const MoleQueue::Message &message); void handleUnknownMethod(const MoleQueue::Message &message); void handleInvalidParams(const MoleQueue::Message &message, const QString &description); void handleListQueuesRequest(const MoleQueue::Message &message); void handleSubmitJobRequest(const MoleQueue::Message &message); void handleCancelJobRequest(const MoleQueue::Message &message); void handleLookupJobRequest(const MoleQueue::Message &message); void handleRegisterOpenWithRequest(const MoleQueue::Message &message); void handleListOpenWithNamesRequest(const MoleQueue::Message &message); void handleUnregisterOpenWithRequest(const MoleQueue::Message &message); void handleRpcKillRequest(const MoleQueue::Message &message); /**@}*/ protected: /** * @brief timerEvent Reimplemented from QObject. */ void timerEvent(QTimerEvent *); /// List of active connections QList m_connections; /// The JobManager for this Server. JobManager *m_jobManager; /// The QueueManager for this Server. QueueManager *m_queueManager; /// The JsonRpc listener for this Server. JsonRpc *m_jsonrpc; /// Local directory for running jobs. QString m_workingDirectoryBase; /// Counter for MoleQueue job ids. IdType m_moleQueueIdCounter; // job id --> connection for notifications. QMap m_connectionLUT; // job id --> reply to endpoint for notifications QMap m_endpointLUT; private: void createConnectionListeners(); QString m_serverName; /// The connection listeners QList m_connectionListeners; /// The timer id for the job sync event. jobManager()->syncJobState() is /// call regularly by this timer. int m_jobSyncTimer; }; } // end namespace MoleQueue #endif // MOLEQUEUE_SERVER_H molequeue-0.9.0/molequeue/app/sshcommand.cpp000066400000000000000000000121171323436134600211510ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "sshcommand.h" #include "logger.h" #include "terminalprocess.h" #include #include #include namespace MoleQueue { SshCommand::SshCommand(QObject *parentObject, QString ssh, QString scp) : SshConnection(parentObject), m_sshCommand(ssh), m_scpCommand(scp), m_exitCode(-1), m_process(0), m_isComplete(true) { } SshCommand::~SshCommand() { delete m_process; m_process = 0; } QString SshCommand::output() const { return isComplete() ? m_output : QString(); } int SshCommand::exitCode() const { return isComplete() ? m_exitCode : -1; } bool SshCommand::waitForCompletion(int msecs) { if (!m_process) return false; if (m_process->state() == QProcess::Starting) m_process->waitForStarted(msecs); if (m_isComplete) return true; return m_process->waitForFinished(msecs); } bool SshCommand::isComplete() const { return m_isComplete; } bool SshCommand::execute(const QString &command) { if (!isValid()) return false; QStringList args = sshArgs(); args << remoteSpec() << command; sendRequest(m_sshCommand, args); return true; } bool SshCommand::copyTo(const QString &localFile, const QString &remoteFile) { if (!isValid()) return false; QStringList args = scpArgs(); QString remoteFileSpec = remoteSpec() + ":" + remoteFile; args << localFile << remoteFileSpec; sendRequest(m_scpCommand, args); return true; } bool SshCommand::copyFrom(const QString &remoteFile, const QString &localFile) { if (!isValid()) return false; QStringList args = scpArgs(); QString remoteFileSpec = remoteSpec() + ":" + remoteFile; args << remoteFileSpec << localFile; sendRequest(m_scpCommand, args); return true; } bool SshCommand::copyDirTo(const QString &localDir, const QString &remoteDir) { if (!isValid()) return false; QStringList args = scpArgs(); QString remoteDirSpec = remoteSpec() + ":" + remoteDir; args << "-r" << localDir << remoteDirSpec; sendRequest(m_scpCommand, args); return true; } bool SshCommand::copyDirFrom(const QString &remoteDir, const QString &localDir) { if (!isValid()) return false; QDir local(localDir); if (!local.exists()) local.mkpath(localDir); /// @todo Check for failure of mkpath QStringList args = scpArgs(); QString remoteDirSpec = remoteSpec() + ":" + remoteDir; args << "-r" << remoteDirSpec << localDir; sendRequest(m_scpCommand, args); return true; } void SshCommand::processStarted() { m_process->closeWriteChannel(); emit requestSent(); } void SshCommand::processFinished() { m_output = m_process->readAll(); m_exitCode = m_process->exitCode(); m_process->close(); if (debug()) { Logger::logDebugMessage(tr("SSH finished (%1) Exit code: %2\n%3") .arg(reinterpret_cast(this)) .arg(m_exitCode).arg(m_output)); } m_isComplete = true; emit requestComplete(); } void SshCommand::sendRequest(const QString &command, const QStringList &args) { if (!m_process) initializeProcess(); m_isComplete = false; if (debug()) { Logger::logDebugMessage(tr("SSH request (%1): %2 %3") .arg(reinterpret_cast(this)) .arg(command).arg(args.join((" ")))); } m_process->start(command, args); } void SshCommand::initializeProcess() { // Initialize the environment for the process, set merged channels. if (!m_process) m_process = new TerminalProcess(this); QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); QProcessEnvironment sshEnv; if (env.contains("DISPLAY")) sshEnv.insert("DISPLAY", env.value("DISPLAY")); if (env.contains("EDITOR")) sshEnv.insert("EDITOR", env.value("EDITOR")); if (env.contains("SSH_AUTH_SOCK")) sshEnv.insert("SSH_AUTH_SOCK", env.value("SSH_AUTH_SOCK")); if (env.contains("KRB5CCNAME")) sshEnv.insert("KRB5CCNAME", env.value("KRB5CCNAME")); if (env.contains("SSH_ASKPASS")) sshEnv.insert("SSH_ASKPASS", env.value("SSH_ASKPASS")); m_process->setProcessEnvironment(sshEnv); m_process->setProcessChannelMode(QProcess::MergedChannels); connect(m_process, SIGNAL(started()), this, SLOT(processStarted())); connect(m_process, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(processFinished())); } QString SshCommand::remoteSpec() { return m_userName.isEmpty() ? m_hostName : m_userName + "@" + m_hostName; } } // End namespace molequeue-0.9.0/molequeue/app/sshcommand.h000066400000000000000000000134531323436134600206220ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef SSHCOMMAND_H #define SSHCOMMAND_H #include "sshconnection.h" #include namespace MoleQueue { class TerminalProcess; /** * @class SshCommand sshcommand.h * @brief Abstract subclass of SshConnection providing base implementaton using * commandline ssh/scp. * * @author Marcus D. Hanwell, David C. Lonie, Chris Harris * * The SshCommand provides an base implementation of the SshConnection interface * that calls the commandline ssh and scp executables in a TerminalProcess. * * When writing code that needs ssh functionality, the code should use the * SshConnection interface instead. */ class SshCommand : public SshConnection { Q_OBJECT public: SshCommand(QObject *parentObject, QString sshCommand, QString scpCommand); ~SshCommand(); /** \return The SSH command that will be run. */ QString sshCommand() { return m_sshCommand; } /** \return The SCP command that will be run. */ QString scpCommand() { return m_scpCommand; } /** \return The merged stdout and stderr of the remote command */ QString output() const; /** \return The exit code returned from the remote command. */ int exitCode() const; /** * Wait until the request has been completed. * * @param msecs Timeout in milliseconds. Default is 30 seconds. * * @return True if request finished, false on timeout. */ bool waitForCompletion(int msecs = 30000); /** @return True if the request has completed. False otherwise. */ bool isComplete() const; public slots: /** * Set the SSH command for the class. Defaults to 'ssh', and would execute * the SSH commnand in the user's path. */ void setSshCommand(const QString &command) { m_sshCommand = command; } /** * Set the SCP command for the class. Defaults to 'scp', and would execute * the SCP commnand in the user's path. */ void setScpCommand(const QString &command) { m_scpCommand = command; } /** * Execute the supplied command on the remote host. * * \note The command is executed asynchronously, see requestComplete() or * waitForCompletion() for results. * * \sa requestSent() requestCompleted() waitForCompeletion() * * \param command The command to execute. * \return True on success, false on failure. */ virtual bool execute(const QString &command); /** * Copy a local file to the remote system. * * \note The command is executed asynchronously, see requestComplete() or * waitForCompletion() for results. * * \sa requestSent() requestCompleted() waitForCompeletion() * * \param localFile The path of the local file. * \param remoteFile The path of the file on the remote system. * \return True on success, false on failure. */ virtual bool copyTo(const QString &localFile, const QString &remoteFile); /** * Copy a remote file to the local system. * * \note The command is executed asynchronously, see requestComplete() or * waitForCompletion() for results. * * \sa requestSent() requestCompleted() waitForCompeletion() * * \param remoteFile The path of the file on the remote system. * \param localFile The path of the local file. * \return True on success, false on failure. */ virtual bool copyFrom(const QString &remoteFile, const QString &localFile); /** * Copy a local directory recursively to the remote system. * * \note The command is executed asynchronously, see requestComplete() or * waitForCompletion() for results. * * \sa requestSent() requestCompleted() waitForCompeletion() * * \param localDir The path of the local directory. * \param remoteDir The path of the directory on the remote system. * \return True on success, false on failure. */ virtual bool copyDirTo(const QString &localDir, const QString &remoteDir); /** * Copy a remote directory recursively to the local system. * * \note The command is executed asynchronously, see requestComplete() or * waitForCompletion() for results. * * \sa requestSent() requestCompleted() waitForCompeletion() * * \param remoteDir The path of the directory on the remote system. * \param localFile The path of the local directory. * \return True on success, false on failure. */ virtual bool copyDirFrom(const QString &remoteDir, const QString &localDir); protected slots: /// Called when the TerminalProcess enters the Running state. void processStarted(); /// Called when the TerminalProcess exits the Running state. void processFinished(); protected: /// Send a request. This launches the process and connects the completion /// signals virtual void sendRequest(const QString &command, const QStringList &args); /// Initialize the TerminalProcess object. void initializeProcess(); /// @return the arguments to be passed to the SSH command. virtual QStringList sshArgs() = 0; /// @return the arguments to be passed to the SCP command. virtual QStringList scpArgs() = 0; /// @return the remote specification, e.g. "user@host" or "host" QString remoteSpec(); QString m_sshCommand; QString m_scpCommand; QString m_output; int m_exitCode; TerminalProcess *m_process; bool m_isComplete; }; } // End namespace #endif molequeue-0.9.0/molequeue/app/sshcommandfactory.cpp000066400000000000000000000041141323436134600225370ustar00rootroot00000000000000/****************************************************************************** This source file is part of the Avogadro project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "sshcommandfactory.h" #include "puttycommand.h" #include "opensshcommand.h" #include #include namespace MoleQueue { namespace { static SshCommandFactory *factoryInstance; } SshCommandFactory::SshCommandFactory(QObject *parentObject) : QObject(parentObject) { } SshCommandFactory *SshCommandFactory::instance() { static QMutex mutex; if (!factoryInstance) { mutex.lock(); if (!factoryInstance) factoryInstance = new SshCommandFactory(QCoreApplication::instance()); mutex.unlock(); } return factoryInstance; } SshCommand *SshCommandFactory::newSshCommand(QObject *parentObject) { #ifdef _WIN32 return newSshCommand(Putty, parentObject); #else return newSshCommand(OpenSsh, parentObject); #endif } SshCommand *SshCommandFactory::newSshCommand(SshClient sshClient, QObject *parentObject) { switch(sshClient) { case OpenSsh: return new OpenSshCommand(parentObject); #ifdef _WIN32 case Putty: return new PuttyCommand(parentObject); #endif default: qFatal("Can not create ssh command for: %d", sshClient); return NULL; } } QString SshCommandFactory::defaultSshCommand() { #ifdef _WIN32 return QString("plink"); #else return QString("ssh"); #endif } QString SshCommandFactory::defaultScpCommand() { #ifdef _WIN32 return QString("pscp"); #else return QString("scp"); #endif } } // End MoleQueue namespace molequeue-0.9.0/molequeue/app/sshcommandfactory.h000066400000000000000000000030651323436134600222100ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef SSHCOMMANDFACTORY_H #define SSHCOMMANDFACTORY_H #include "sshcommand.h" namespace MoleQueue { /** * @class SshCommandFactory sshcommandfactory.h * @brief Used to construct the correct SshCommand implementation based on the * ssh client * * @author Chris Harris * */ class SshCommandFactory: public QObject { Q_OBJECT public: /// Ssh clients enum SshClient { OpenSsh #ifdef _WIN32 , Putty #endif }; static SshCommandFactory *instance(); static QString defaultSshCommand(); static QString defaultScpCommand(); /** * @return a new SshCommand for this platform, the caller is responsible * for cleanup */ SshCommand *newSshCommand(QObject *parentObject = 0); SshCommand *newSshCommand(SshClient sshClient, QObject *parentObject = 0); private: SshCommandFactory(QObject *parentObject = 0); }; } // End namespace #endif molequeue-0.9.0/molequeue/app/sshconnection.cpp000066400000000000000000000034501323436134600216720ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "sshconnection.h" #include namespace MoleQueue { SshConnection::SshConnection(QObject *parentObject) : QObject(parentObject), m_persistent(false), m_portNumber(-1) { } SshConnection::~SshConnection() { } bool SshConnection::isValid() const { if (m_hostName.isEmpty()) return false; else return true; } QString SshConnection::output() const { return ""; } int SshConnection::exitCode() const { return -1; } bool SshConnection::waitForCompletion(int) { return false; } bool SshConnection::isComplete() const { return false; } bool SshConnection::execute(const QString &) { // Always fails in the base class - no valid transport. return false; } bool SshConnection::copyTo(const QString &, const QString &) { return false; } bool SshConnection::copyFrom(const QString &, const QString &) { return false; } bool SshConnection::copyDirTo(const QString &, const QString &) { return false; } bool SshConnection::copyDirFrom(const QString &, const QString &) { return false; } bool SshConnection::debug() { const char *val = qgetenv("MOLEQUEUE_DEBUG_SSH"); return (val != NULL && val[0] != '\0'); } } // End of namespace molequeue-0.9.0/molequeue/app/sshconnection.h000066400000000000000000000151651323436134600213450ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011-2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef SSHCONNECTION_H #define SSHCONNECTION_H #include #include namespace MoleQueue { /** * @class SshConnection sshconnection.h * @brief Abstract base class defining remote execution and file transfer * operations over the ssh protocol. * @author Marcus D. Hanwell, David C. Lonie * * The SshConnection is the interface to use when writing code that requires * interactions with a remote host. Subclasses provide concrete implementations * of the interface, e.g. SshCommand, which calls the ssh and scp commands in a * TerminalProcess. */ class SshConnection : public QObject { Q_OBJECT public: SshConnection(QObject *parentObject = 0); ~SshConnection(); /** \return If the SSH connection is set as persistent or not. */ bool isPersistent() const { return m_persistent; } /** \return The user name that will be used. */ QString userName() const { return m_userName; } /** \return The host that will be used. */ QString hostName() const { return m_hostName; } /** \return The path to the identity file that will be used. */ QString identityFile() const { return m_identityFile; } /** \return The port that will be used. */ int portNumber() const { return m_portNumber; } /** \return Whether the connection is valid, at a minimum need a host name. */ virtual bool isValid() const; /** \return The merged stdout and stderr of the remote command. */ virtual QString output() const; /** \return The exit code returned from a remote command. */ virtual int exitCode() const; /** * Wait until the request has been completed. * * @param msecs Timeout in milliseconds. Default is 30 seconds. * * @return True if request finished, false on timeout. */ virtual bool waitForCompletion(int msecs = 30000); /** @return True if the request has completed. False otherwise. */ virtual bool isComplete() const; /** @return A reference to arbitrary data stored in the command. */ QVariant & data() {return m_data;} /** @return A reference to arbitrary data stored in the command. */ const QVariant & data() const {return m_data;} /** @param newData Arbitrary data to store in the command. */ void setData(const QVariant &newData) {m_data = newData;} public slots: /** * Set whether the connection should be persistent, or each issuesd command * uses a short-lived connection, e.g. on the command line a non-persistent * connection would be the equivalent of, * * ssh user@host ls */ void setPersistent(bool persist) { m_persistent = persist; } /** * Set the user name to use for the connection. */ void setUserName(const QString &newUserName) { m_userName = newUserName; } /** * Set the host name to use for the connection. */ void setHostName(const QString &newHostName) { m_hostName = newHostName; } /** * Set the identity file to use for the connection. This is the path to the * private key to be used when establishing the connection. */ void setIdentityFile(const QString &newIdentityFile) { m_identityFile = newIdentityFile; } /** * Set the host name to use for the connection. */ void setPortNumber(int newPortNumber) { m_portNumber = newPortNumber; } /** * Execute the supplied command on the remote host. * * \note The command is executed asynchronously, see requestComplete() or * waitForCompletion() for results. * * \sa requestSent() requestCompleted() waitForCompeletion() * * \param command The command to execute. * * \return True on success, false on failure. */ virtual bool execute(const QString &command); /** * Copy a local file to the remote system. * * \note The command is executed asynchronously, see requestComplete() or * waitForCompletion() for results. * * \sa requestSent() requestCompleted() waitForCompeletion() * * \param localFile The path of the local file. * \param remoteFile The path of the file on the remote system. * \return True on success, false on failure. */ virtual bool copyTo(const QString &localFile, const QString &remoteFile); /** * Copy a remote file to the local system. * * \note The command is executed asynchronously, see requestComplete() or * waitForCompletion() for results. * * \sa requestSent() requestCompleted() waitForCompeletion() * * \param remoteFile The path of the file on the remote system. * \param localFile The path of the local file. * \return True on success, false on failure. */ virtual bool copyFrom(const QString &remoteFile, const QString &localFile); /** * Copy a local directory recursively to the remote system. * * \note The command is executed asynchronously, see requestComplete() or * waitForCompletion() for results. * * \sa requestSent() requestCompleted() waitForCompeletion() * * \param localDir The path of the local directory. * \param remoteDir The path of the directory on the remote system. * \return True on success, false on failure. */ virtual bool copyDirTo(const QString &localDir, const QString &remoteDir); /** * Copy a remote directory recursively to the local system. * * \note The command is executed asynchronously, see requestComplete() or * waitForCompletion() for results. * * \sa requestSent() requestCompleted() waitForCompeletion() * * \param remoteDir The path of the directory on the remote system. * \param localFile The path of the local directory. * \return True on success, false on failure. */ virtual bool copyDirFrom(const QString &remoteDir, const QString &localDir); signals: /** * Emitted when the request has been sent to the server. */ void requestSent(); /** * Emitted when the request has been sent and the reply (if any) received. */ void requestComplete(); protected: static bool debug(); bool m_persistent; QVariant m_data; QString m_userName; QString m_hostName; QString m_identityFile; int m_portNumber; }; } // End of namespace #endif molequeue-0.9.0/molequeue/app/templatekeyworddialog.cpp000066400000000000000000000206221323436134600234150ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "templatekeyworddialog.h" #include "ui_templatekeyworddialog.h" #include #include #include namespace MoleQueue { TemplateKeywordDialog::TemplateKeywordDialog(QWidget *parent_) : QDialog(parent_), ui(new Ui::TemplateKeywordDialog) { ui->setupUi(this); m_docHeaderBlockFormat.setAlignment(Qt::AlignHCenter); m_docHeaderBlockFormat.setTopMargin(-5); m_docHeaderCharFormat.setFontPointSize(12); m_helpTextBlockFormat.setAlignment(Qt::AlignJustify); m_helpTextBlockFormat.setTextIndent(20); m_keywordHeaderBlockFormat.setAlignment(Qt::AlignHCenter); m_keywordHeaderBlockFormat.setTopMargin(10); m_keywordHeaderBlockFormat.setBottomMargin(5); m_keywordHeaderCharFormat.setFontPointSize(10); m_keywordListBlockFormat.setAlignment(Qt::AlignJustify); m_keywordListBlockFormat.setTextIndent(-40); m_keywordListBlockFormat.setIndent(1); m_keywordCharFormat.setForeground(QBrush(Qt::blue)); m_keywordCharFormat.setFontItalic(true); m_dangerousKeywordCharFormat.setForeground(QBrush(Qt::darkRed)); m_keywordCharFormat.setFontItalic(true); buildKeywordLists(); buildDocument(); } TemplateKeywordDialog::~TemplateKeywordDialog() { delete ui; } void TemplateKeywordDialog::buildKeywordLists() { // Jobs m_jobKeywords.insert("$$inputFileName$$", tr("Name of the current job's input file.")); m_jobKeywords.insert("$$inputFileBaseName$$", tr("Name of the current job's input file without the " "file extension.")); m_jobKeywords.insert("$$moleQueueId$$", tr("MoleQueue ID number of current job.")); m_jobKeywords.insert("$$numberOfCores$$", tr("Number of processor cores requested by current " "job.")); m_jobKeywords.insert("$$maxWallTime$$", tr("The maximum walltime for the current job (i.e. the " "time limit before the queue will automatically " "stop the job, regardless of completion state). If " "the job's specified walltime is less than or equal " "to zero minutes, the default " "walltime (configured in the queue settings) is used." " See $$$maxWallTime$$$ for a method of using the " "default walltime set by the queue administrator. " "Available only on remote queues.")); m_jobKeywords.insert("$$$maxWallTime$$$", tr("Same as $$maxWallTime$$, but if the job specific " "walltime is not set, the entire line containing " "this keyword will be removed from the final " "template output. This is used to apply " "the default walltime set by the queuing system's " "administrator. Only available on remote queuing " "systems.")); m_jobKeywords.insert(tr("Custom"), tr("Certain clients may allow custom keyword " "replacements in their jobs. Consult the client " "documentation to see if these are available and how " "they are to be specified in the template.")); // Queue m_queueKeywords.insert("$$programExecution$$", tr("Used in remote queue templates to indicate where " "to place program-specific executable details (e.g." " where something like '[executable] < [inputfile]'" " should be placed). Must only be used in a queue " "configuration template (this keyword replacement " "is used to generate the program specific " "template).")); } void TemplateKeywordDialog::buildDocument() { QTextCursor cur(ui->textEdit->document()); cur.movePosition(QTextCursor::Start); cur.beginEditBlock(); // Doc header addDocumentHeader(tr("Templates in MoleQueue"), cur); // Help text addHelpText(tr("Templates are used to specify how " "programs are started on a each queue, and are " "customized in two places in MoleQueue:\n" "Non-local queues " "(e.g. PBS, SGE, etc) use batch scripts to specify " "program execution, and a template for a queue batch " "script is entered in the remote queue configuration, " "using the $$programExecution$$ keyword to indicate " "where program-specific execution should go.\n" "The program configuration dialog allows further " "customization of the input template, providing a set " "of common execution methods and the option to " "customize them. The program-specific input template " "may completely override the queue template, but will " "use it as a starting point initially.\n" "The following list of keywords may be used in the " "input templates and are replaced by information " "appropriate to a specific job. Keywords are enclosed " "in '$$' or '$$$' and are case sensitive. Keywords with two " "'$' symbols will be replaced by the appropriate data, while " " those with three '$' have more specialized behavior (" "see the maxWallTime variants for an example).\n" "Any unrecognized keywords that are not replaced during script" " generation will be removed and a warning printed to the " "log."), cur); // Jobs addKeywordHeader(tr("Job specific keywords:"), cur); addKeywordMap(m_jobKeywords, cur); // Queue addKeywordHeader(tr("Queue specific keywords:"), cur); addKeywordMap(m_queueKeywords, cur); cur.endEditBlock(); highlightKeywords(); } void TemplateKeywordDialog::addDocumentHeader(const QString &header, QTextCursor &cur) { cur.insertBlock(m_docHeaderBlockFormat); cur.setCharFormat(m_docHeaderCharFormat); cur.insertText(header); } void TemplateKeywordDialog::addHelpText(const QString &text, QTextCursor &cur) { cur.insertBlock(m_helpTextBlockFormat); cur.setCharFormat(m_helpTextCharFormat); cur.insertText(text); } void TemplateKeywordDialog::addKeywordHeader(const QString &header, QTextCursor &cur) { cur.insertBlock(m_keywordHeaderBlockFormat); cur.setCharFormat(m_keywordHeaderCharFormat); cur.insertText(header); } void TemplateKeywordDialog::addKeywordMap(const QMap &map, QTextCursor &cur) { foreach (const QString &keyword, map.keys()) { const QString &desc = map[keyword]; cur.insertBlock(m_keywordListBlockFormat); cur.setCharFormat(m_keywordDescriptionCharFormat); cur.insertText(QString("%1: %2").arg(keyword, desc)); } } void TemplateKeywordDialog::highlightKeywords() { QTextDocument *doc = ui->textEdit->document(); QTextCursor cur(doc); cur.movePosition(QTextCursor::Start); QRegExp expr("[^\\$]?\\${2,2}[^\\$\\s]+\\${2,2}[^\\$]?"); cur = doc->find(expr, cur); while (!cur.isNull()) { cur.setCharFormat(m_keywordCharFormat); cur = doc->find(expr, cur); } cur.movePosition(QTextCursor::Start); expr.setPattern("[^\\$]?\\${3,3}[^\\$\\s]+\\${3,3}[^\\$]?"); cur = doc->find(expr, cur); while (!cur.isNull()) { cur.setCharFormat(m_dangerousKeywordCharFormat); cur = doc->find(expr, cur); } } } // namespace MoleQueue molequeue-0.9.0/molequeue/app/templatekeyworddialog.h000066400000000000000000000041211323436134600230560ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_TEMPLATEKEYWORDDIALOG_H #define MOLEQUEUE_TEMPLATEKEYWORDDIALOG_H #include #include #include #include namespace Ui { class TemplateKeywordDialog; } namespace MoleQueue { /// @brief Dialog explaining how templates are used in MoleQueue. class TemplateKeywordDialog : public QDialog { Q_OBJECT public: explicit TemplateKeywordDialog(QWidget *parent_ = 0); ~TemplateKeywordDialog(); private: void buildKeywordLists(); void buildDocument(); void addDocumentHeader(const QString &header, QTextCursor &cur); void addHelpText(const QString &text, QTextCursor &cur); void addKeywordHeader(const QString &header, QTextCursor &cur); void addKeywordMap(const QMap &map, QTextCursor &cur); void highlightKeywords(); Ui::TemplateKeywordDialog *ui; QTextBlockFormat m_docHeaderBlockFormat; QTextBlockFormat m_helpTextBlockFormat; QTextBlockFormat m_keywordHeaderBlockFormat; QTextBlockFormat m_keywordListBlockFormat; QTextCharFormat m_docHeaderCharFormat; QTextCharFormat m_helpTextCharFormat; QTextCharFormat m_keywordHeaderCharFormat; QTextCharFormat m_keywordDescriptionCharFormat; QTextCharFormat m_keywordCharFormat; QTextCharFormat m_dangerousKeywordCharFormat; QMap m_jobKeywords; QMap m_queueKeywords; }; } // namespace MoleQueue #endif // MOLEQUEUE_TEMPLATEKEYWORDDIALOG_H molequeue-0.9.0/molequeue/app/terminalprocess.cpp000066400000000000000000000022121323436134600222220ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "terminalprocess.h" #ifdef Q_OS_UNIX # include #endif namespace MoleQueue { TerminalProcess::TerminalProcess(QObject *parentObject) : QProcess(parentObject) { } TerminalProcess::~TerminalProcess() { } void TerminalProcess::setupChildProcess() { #ifdef Q_OS_UNIX // Become the session leader on Unix (no-op on Windows). This makes things // like SSH use GUIs to prompt for passwords (SSH_ASKPASS) as there is no // tty associated with the process. setsid(); #endif } } // End namespace molequeue-0.9.0/molequeue/app/terminalprocess.h000066400000000000000000000023061323436134600216730ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2011 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef TERMINALPROCESS_H #define TERMINALPROCESS_H #include namespace MoleQueue { /** * @class TerminalProcess terminalprocess.h * @brief Special QProcess derived class, calls setsid on Unix to remove tty, * allowing us to give a GUI prompt for SSH etc. * @author Marcus D. Hanwell */ class TerminalProcess : public QProcess { Q_OBJECT public: explicit TerminalProcess(QObject *parentObject = 0); ~TerminalProcess(); protected: virtual void setupChildProcess(); }; } // End namespace #endif // TERMINALPROCESS_H molequeue-0.9.0/molequeue/app/testing/000077500000000000000000000000001323436134600177645ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/CMakeLists.txt000066400000000000000000000035341323436134600225310ustar00rootroot00000000000000include_directories(${CMAKE_SOURCE_DIR}/molequeue ${CMAKE_BINARY_DIR}/molequeue/client ${CMAKE_BINARY_DIR}/molequeue/app/testing) set(MoleQueue_HAS_ZMQ ${USE_ZERO_MQ}) set(MoleQueue_TESTDATA_DIR "${MoleQueue_SOURCE_DIR}/molequeue/app/testing/data/") set(MoleQueue_TESTSCRIPT_DIR "${MoleQueue_SOURCE_DIR}/molequeue/app/testing/scripts/") set(MoleQueue_TESTEXEC_DIR "${MoleQueue_BINARY_DIR}/bin/") if(PYTHON_EXECUTABLE) set(MoleQueue_PYTHON_EXECUTABLE "${PYTHON_EXECUTABLE}") endif() configure_file(molequeuetestconfig.h.in molequeuetestconfig.h) if(USE_ZERO_MQ) add_definitions(-DUSE_ZERO_MQ) endif() set(testutils_SRCS dummyconnection.cpp dummyconnectionlistener.cpp dummyqueuemanager.cpp dummyqueueremote.cpp dummyserver.cpp dummysshcommand.cpp referencestring.cpp testserver.cpp xmlutils.cpp ) add_library(testutils STATIC ${testutils_SRCS}) set_target_properties(testutils PROPERTIES AUTOMOC TRUE) qt5_use_modules(testutils Test) target_link_libraries(testutils molequeue_static) set(MyTests filespecification jobmanager jsonrpc message pbs program queue queuemanager queueremote server sge slurm sshcommand ) # This test is currently only configured to run on unix if(NOT WIN32) list(APPEND MyTests clientserver) endif() if(MoleQueue_USE_EZHPC_UIT) list(APPEND MyTests authenticatecont authenticateresponse compositeiodevice dirlistinginfo filestreamingdata jobeventlist jobsubmissioninfo kerberoscredentials uit userhostassoclist) endif() foreach(test ${MyTests}) add_executable(${test}test MACOSX_BUNDLE ${test}test.cpp) qt5_use_modules(${test}test Test) set_target_properties(${test}test PROPERTIES AUTOMOC TRUE) target_link_libraries(${test}test testutils) add_test(NAME molequeue-${test} COMMAND ${test}test) endforeach() add_subdirectory(clienttestsrc) molequeue-0.9.0/molequeue/app/testing/authenticateconttest.cpp000066400000000000000000000037251323436134600247410ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include #include "queues/uit/authenticatecont.h" #include "queues/uit/authenticateresponse.h" #include "referencestring.h" #include "xmlutils.h" class AuthenticateContTest : public QObject { Q_OBJECT private slots: void testToXmlNoReply(); void testToXmlWithReply(); }; void AuthenticateContTest::testToXmlNoReply() { QList prompts; prompts << MoleQueue::Uit::Prompt(0, "prompt1"); prompts << MoleQueue::Uit::Prompt(1, "prompt2"); QString id = "sessionId"; MoleQueue::Uit::AuthenticateCont authCont(id, prompts); ReferenceString expected( "authenticatecont-ref/authenticatecont-no-reply.xml"); QCOMPARE(authCont.toXml(), XmlUtils::stripWhitespace(expected)); } void AuthenticateContTest::testToXmlWithReply() { QList prompts; MoleQueue::Uit::Prompt prompt1(0, "prompt1"); prompt1.setUserResponse("reply1"); MoleQueue::Uit::Prompt prompt2(1, "prompt2"); prompt2.setUserResponse("reply2"); prompts << prompt1 << prompt2; QString id = "sessionId"; MoleQueue::Uit::AuthenticateCont authCont(id, prompts); ReferenceString expected( "authenticatecont-ref/authenticatecont-reply.xml"); QCOMPARE(authCont.toXml(), XmlUtils::stripWhitespace(expected)); } QTEST_MAIN(AuthenticateContTest) #include "authenticateconttest.moc" molequeue-0.9.0/molequeue/app/testing/authenticateresponsetest.cpp000066400000000000000000000052741323436134600256350ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include #include "queues/uit/authenticateresponse.h" #include "referencestring.h" #include "xmlutils.h" class AuthenticateResponseTest : public QObject { Q_OBJECT private slots: void initTestCase(); void testFromXml(); void testXpathExpressions(); private: QString m_authenticateResponseXml; }; void AuthenticateResponseTest::initTestCase() { m_authenticateResponseXml = XmlUtils::stripWhitespace( ReferenceString("authenticateresponse-ref/authenticateresponse.xml")); } void AuthenticateResponseTest::testFromXml() { MoleQueue::Uit::AuthenticateResponse response = MoleQueue::Uit::AuthenticateResponse::fromXml(m_authenticateResponseXml); QVERIFY(response.hasPrompts()); QCOMPARE(response.authSessionId(), QString("FE09938C-84BC-E75A-D767-84B85F48C4DB")); QCOMPARE(response.errorMessage(), QString("error")); QCOMPARE(response.prompts().size(), 2); QCOMPARE(response.prompts()[0].id(), 0); QCOMPARE(response.prompts()[0].prompt(), QString("SecurID Passcode")); QCOMPARE(response.prompts()[1].id(), 2); QCOMPARE(response.prompts()[1].prompt(), QString("Password")); } void AuthenticateResponseTest::testXpathExpressions() { QXmlQuery query; query.setFocus(m_authenticateResponseXml); query.setQuery("/AuthenticateResponse/success/string()"); QString result; query.evaluateTo(&result); QCOMPARE(result.trimmed(), QString("false")); QStringList expectedIds; expectedIds << "0" << "2"; QStringList expectedPrompts; expectedPrompts << "SecurID Passcode" << "Password"; QStringList ids; query.setQuery("/AuthenticateResponse/prompts/Prompt/id/string()"); int index = 0; foreach (const QString &id, ids) { QCOMPARE(id, expectedIds[index]); query.bindVariable("id", QVariant(id)); query.setQuery( QString("/AuthenticateResponse/prompts/Prompt[id=$id]/prompt/string()")); QString prompt; query.evaluateTo(&prompt); QCOMPARE(prompt, expectedPrompts[index++]); } } QTEST_MAIN(AuthenticateResponseTest) #include "authenticateresponsetest.moc" molequeue-0.9.0/molequeue/app/testing/clientservertest.cpp000066400000000000000000000225351323436134600241040ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include "filesystemtools.h" #include "molequeuetestconfig.h" #include "testserver.h" // for getRandomSocketName #include #include // Define ENABLE_ZMQ_TESTS if both zeromq and python are available #ifdef MoleQueue_PYTHON_EXECUTABLE #ifdef MoleQueue_HAS_ZMQ #define ENABLE_ZMQ_TESTS #endif // MoleQueue_HAS_ZMQ #endif // MoleQueue_PYTHON_EXECUTABLE class ClientServerTest : public QObject { Q_OBJECT public: ClientServerTest() : QObject(NULL), m_numClients(10), m_workDir(MoleQueue_BINARY_DIR "/testworkdir"), m_moleQueueExecutable(MoleQueue_BINARY_DIR "/bin/molequeue"), m_serverProcess(NULL) { #ifdef __APPLE__ m_moleQueueExecutable = MoleQueue_BINARY_DIR "/bin/molequeue.app/Contents/" "MacOS/molequeue"; #endif // __APPLE__ randomizeSocketName(); } private: int m_numClients; QString m_workDir; QString m_socketName; QString m_moleQueueExecutable; QStringList m_moleQueueDefaultArgs; QProcess *m_serverProcess; QList m_clientProcesses; /// Delete the testing workdir and initialize it with the directory at /// @a sourcePath. bool resetWorkDir(const QString &sourcePath); /// Create a new randomized socket name, stored in m_socketName; void randomizeSocketName(); /// Create the server process (m_serverProcess) and reset /// m_moleQueueDefaultArgs to set the workdir, socketname, and enable rpcKill. bool setupServerProcess(); /// Create a Cxx client process QProcess *addClientProcess(); #ifdef ENABLE_ZMQ_TESTS /// Create a client process initialized for python. The process is returned /// and added to m_clientProcesses. QProcess *addPythonClientProcess(); #endif // ENABLE_ZMQ_TESTS private slots: /// Called before the first test function is executed. void initTestCase(); /// Called after the last test function is executed. void cleanupTestCase(); /// Called before each test function is executed. void init(); /// Called after every test function. void cleanup(); // Python client tests: #ifdef ENABLE_ZMQ_TESTS void submitOnePy(); void submit200Py(); void submit200FromManyClientsPy(); #endif // ENABLE_ZMQ_TESTS }; bool ClientServerTest::resetWorkDir(const QString &sourcePath) { // Initialize working directory if (QFileInfo(m_workDir).exists()) { if (!MoleQueue::FileSystemTools::recursiveRemoveDirectory(m_workDir)) { qWarning() << "Could not remove old working directory" << m_workDir; return false; } } if (!MoleQueue::FileSystemTools::recursiveCopyDirectory(sourcePath, m_workDir)) { qWarning() << "Could not initialize working directory" << m_workDir << "from" << sourcePath; return false; } return true; } void ClientServerTest::randomizeSocketName() { m_socketName = TestServer::getRandomSocketName(); } bool ClientServerTest::setupServerProcess() { m_moleQueueDefaultArgs.clear(); m_moleQueueDefaultArgs << "--workdir" << m_workDir << "--socketname" << m_socketName << "--rpc-kill"; if (m_serverProcess) { delete m_serverProcess; m_serverProcess = NULL; } m_serverProcess = new QProcess(this); m_serverProcess->setProcessChannelMode(QProcess::ForwardedChannels); return true; } QProcess *ClientServerTest::addClientProcess() { QProcess *clientProcess = new QProcess(this); clientProcess->setProcessChannelMode(::QProcess::ForwardedChannels); m_clientProcesses.append(clientProcess); return clientProcess; } #ifdef ENABLE_ZMQ_TESTS QProcess *ClientServerTest::addPythonClientProcess() { QProcess *clientProcess = addClientProcess(); QProcessEnvironment env = clientProcess->processEnvironment(); env.insert("PYTHONPATH", (env.value("PYTHONPATH").isEmpty() ? QString() : env.value("PYTHONPATH") + ':') + MoleQueue_SOURCE_DIR "/python"); clientProcess->setProcessEnvironment(env); return clientProcess; } #endif // ENABLE_ZMQ_TESTS void ClientServerTest::initTestCase() { QVERIFY2(resetWorkDir(MoleQueue_TESTDATA_DIR "/testworkdir_unix"), "Failed to reset working directory for test."); // Setup server process QVERIFY(setupServerProcess()); // Start server qDebug() << "Starting server:" << m_moleQueueExecutable << m_moleQueueDefaultArgs.join(" "); m_serverProcess->start(m_moleQueueExecutable, m_moleQueueDefaultArgs); QVERIFY(m_serverProcess->waitForStarted(10*1000)); QTest::qSleep(1 * 1000); // Wait one second for server to start } void ClientServerTest::cleanupTestCase() { // send killRpc message QProcess *clientProcess = addClientProcess(); QString clientCommand = MoleQueue_TESTEXEC_DIR "sendRpcKill"; QStringList clientArguments; clientArguments << "-s" << m_socketName; qDebug() << "Starting client:" << clientCommand << clientArguments.join(" "); clientProcess->start(clientCommand, clientArguments); // Wait for client to finish QVERIFY2(clientProcess->waitForFinished(300*1000), "Client timed out."); QCOMPARE(clientProcess->exitCode(), 0); // Wait for server to finish QVERIFY2(m_serverProcess->waitForFinished(5*1000), "Server timed out."); QCOMPARE(m_serverProcess->exitCode(), 0); // In case the rpcKill call fails, kill the process if (m_serverProcess->state() != QProcess::NotRunning) m_serverProcess->kill(); m_serverProcess->deleteLater(); m_serverProcess = NULL; // Clean up the sendRpcKill client. cleanup(); } void ClientServerTest::init() { } void ClientServerTest::cleanup() { foreach (QProcess *proc, m_clientProcesses) { if (proc->state() != QProcess::NotRunning) proc->kill(); } qDeleteAll(m_clientProcesses); m_clientProcesses.clear(); } #ifdef ENABLE_ZMQ_TESTS void ClientServerTest::submitOnePy() { // Setup client process QProcess *clientProcess = addPythonClientProcess(); QString clientCommand = MoleQueue_PYTHON_EXECUTABLE; QStringList clientArguments; clientArguments << MoleQueue_TESTSCRIPT_DIR "/submitJob.py" << "-s" << m_socketName << "-n" << QString::number(1); qDebug() << "Starting client:" << clientCommand << clientArguments.join(" "); clientProcess->start(clientCommand, QStringList(clientArguments)); // Wait 5 seconds for client to start QVERIFY2(clientProcess->waitForStarted(5*1000), "Client did not start."); // Wait 10 seconds for client to finish QVERIFY2(clientProcess->waitForFinished(10*1000), "Client timed out."); QCOMPARE(clientProcess->exitCode(), 0); } void ClientServerTest::submit200Py() { // Setup client process QProcess *clientProcess = addPythonClientProcess(); QString clientCommand = MoleQueue_PYTHON_EXECUTABLE; QStringList clientArguments; clientArguments << MoleQueue_TESTSCRIPT_DIR "/submitJob.py" << "-s" << m_socketName << "-n" << QString::number(200); qDebug() << "Starting client:" << clientCommand << clientArguments.join(" "); clientProcess->start(clientCommand, QStringList(clientArguments)); // Wait 5 seconds for client to start QVERIFY2(clientProcess->waitForStarted(5*1000), "Client did not start."); // Wait one minute for client to finish QVERIFY2(clientProcess->waitForFinished(60*1000), "Client timed out."); QCOMPARE(clientProcess->exitCode(), 0); } void ClientServerTest::submit200FromManyClientsPy() { // Setup client processes while (m_clientProcesses.size() < m_numClients) addPythonClientProcess(); QString clientCommand = MoleQueue_PYTHON_EXECUTABLE; QStringList clientArguments; clientArguments << MoleQueue_TESTSCRIPT_DIR "/submitJob.py" << "-s" << m_socketName << "-n" << QString::number(200); qDebug() << "Starting" << m_numClients << "clients:" << clientCommand << clientArguments.join(" "); int clientId = 0; foreach (QProcess *cliProc, m_clientProcesses) { cliProc->start(clientCommand, QStringList(clientArguments) << "-c" << QString::number(++clientId)); } // Wait 5 seconds for each client to start clientId = 0; foreach (QProcess *cliProc, m_clientProcesses) { ++clientId; QVERIFY2(cliProc->waitForStarted(5*1000), (QByteArray("Client ") + QByteArray::number(clientId) + QByteArray(" failed to start.")).constData()); } // Wait two minutes for all clients to finish clientId = 0; foreach (QProcess *cliProc, m_clientProcesses) { ++clientId; QVERIFY2(cliProc->waitForFinished(2*60*1000), (QByteArray("Client ") + QByteArray::number(clientId) + QByteArray(" timed out.")).constData()); QCOMPARE(cliProc->exitCode(), 0); } } #endif // ENABLE_ZMQ_TESTS QTEST_MAIN(ClientServerTest) #include "clientservertest.moc" molequeue-0.9.0/molequeue/app/testing/clienttestsrc/000077500000000000000000000000001323436134600226525ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/clienttestsrc/CMakeLists.txt000066400000000000000000000006121323436134600254110ustar00rootroot00000000000000include_directories( "${CMAKE_CURRENT_BINARY_DIR}" "${MoleQueue_SOURCE_DIR}/molequeue/client/" "${MoleQueue_BINARY_DIR}/molequeue/client/" ) set(srcs sendRpcKill) foreach(source ${srcs}) add_executable(${source} ${source}.cpp) set_target_properties(${source} PROPERTIES AUTOMOC TRUE) qt5_use_modules(${source} Core) target_link_libraries(${source} MoleQueueClient) endforeach() molequeue-0.9.0/molequeue/app/testing/clienttestsrc/sendRpcKill.cpp000066400000000000000000000030131323436134600255650ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include #include class Killer : public MoleQueue::JsonRpcClient { Q_OBJECT public: Killer(QObject *p = 0) : MoleQueue::JsonRpcClient(p) {} ~Killer() {} void sendRpcKill() { QJsonObject request = emptyRequest(); request["method"] = QLatin1String("rpcKill"); sendRequest(request); } }; int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QString socketName = "MoleQueue"; QStringList args = QCoreApplication::arguments(); for (QStringList::const_iterator it = args.constBegin() + 1, itEnd = args.constEnd(); it != itEnd; ++it) { if (*it == "-s") socketName = *(++it); } Killer killer(&app); killer.connectToServer(socketName); if (!killer.isConnected()) return 1; killer.sendRpcKill(); killer.flush(); return 0; } #include "sendRpcKill.moc" molequeue-0.9.0/molequeue/app/testing/compositeiodevicetest.cpp000066400000000000000000000077421323436134600251140ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include "queues/uit/compositeiodevice.h" #include "referencestring.h" #include "molequeuetestconfig.h" class CompositeIODeviceTest : public QObject { Q_OBJECT private slots: /// Called before each test function is executed. void init(); /// Called after every test function. void cleanup(); void testReadAll(); void testReadSome(); void testReadOver(); void testReadBytes(); void testSize(); void testUploadPattern(); private: MoleQueue::Uit::CompositeIODevice *m_comp; QByteArray *byteArray1; QByteArray *byteArray2; }; void CompositeIODeviceTest::init() { m_comp = new MoleQueue::Uit::CompositeIODevice(this); m_comp->open(QIODevice::ReadOnly); byteArray1 = new QByteArray("abc"); QBuffer *buffer1 = new QBuffer(byteArray1, this); buffer1->open(QIODevice::ReadOnly); m_comp->addDevice(buffer1); byteArray2 = new QByteArray("def"); QBuffer *buffer2 = new QBuffer(byteArray2, this); buffer2->open(QIODevice::ReadOnly); m_comp->addDevice(buffer2); } void CompositeIODeviceTest::cleanup() { delete m_comp; delete byteArray1; delete byteArray2; } void CompositeIODeviceTest::testReadAll() { char data[6]; QVERIFY(m_comp->read(data, 6) == 6); QCOMPARE(QString::fromLocal8Bit(data, 6), QString("abcdef")); } void CompositeIODeviceTest::testReadSome() { char data[2]; QVERIFY(m_comp->read(data, 2) == 2); QCOMPARE(QString::fromLocal8Bit(data, 2), QString("ab")); } void CompositeIODeviceTest::testReadOver() { char data[6]; QVERIFY(m_comp->read(data, 100) == 6); QCOMPARE(QString::fromLocal8Bit(data, 6), QString("abcdef")); } void CompositeIODeviceTest::testReadBytes() { char c; QByteArray bytes; while(m_comp->read(&c, 1) != -1) bytes.append(c); QCOMPARE(QString(bytes), QString("abcdef")); } void CompositeIODeviceTest::testSize() { QVERIFY(m_comp->size() == 6); QString testFilePath = "compositeiodevice-ref/testfile.txt"; ReferenceString fileString(testFilePath); QFile file(MoleQueue_TESTDATA_DIR + testFilePath); file.open(QIODevice::ReadOnly); m_comp->addDevice(&file); QVERIFY(m_comp->size() == file.size()+6); QCOMPARE(QString(m_comp->readAll()), QString("abcdef")+fileString); } void CompositeIODeviceTest::testUploadPattern() { QString xml = "
"; QString testFilePath = "compositeiodevice-ref/testfile.txt"; ReferenceString fileString(testFilePath); QFile file(MoleQueue_TESTDATA_DIR + testFilePath); file.open(QIODevice::ReadWrite); MoleQueue::Uit::CompositeIODevice *dataStream = new MoleQueue::Uit::CompositeIODevice(this); dataStream->open(QIODevice::ReadWrite); QBuffer *headerBuffer = new QBuffer(dataStream); headerBuffer->open(QIODevice::ReadWrite); QTextStream headerStream(headerBuffer); headerStream << xml.size(); headerStream << "|"; headerStream << xml; headerStream << file.size(); headerStream << "|"; headerStream.flush(); headerBuffer->seek(0); dataStream->addDevice(headerBuffer); dataStream->addDevice(&file); QByteArray bytes; char c; while(dataStream->read(&c, 1) > 0) { bytes.append(c); } QCOMPARE(QString(bytes), QString("%1|
%2|%3") .arg(xml.length()) .arg(QString(fileString).length()) .arg(fileString)); } QTEST_MAIN(CompositeIODeviceTest) #include "compositeiodevicetest.moc" molequeue-0.9.0/molequeue/app/testing/data/000077500000000000000000000000001323436134600206755ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/authenticatecont-ref/000077500000000000000000000000001323436134600250115ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/authenticatecont-ref/authenticatecont-no-reply.xml000066400000000000000000000004551323436134600326440ustar00rootroot00000000000000 sessionId 0 prompt1 1 prompt2 molequeue-0.9.0/molequeue/app/testing/data/authenticatecont-ref/authenticatecont-reply.xml000066400000000000000000000004711323436134600322300ustar00rootroot00000000000000 sessionId 0 prompt1 reply1 1 prompt2 reply2 molequeue-0.9.0/molequeue/app/testing/data/authenticateresponse-ref/000077500000000000000000000000001323436134600257045ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/authenticateresponse-ref/authenticateresponse.xml000066400000000000000000000007711323436134600326700ustar00rootroot00000000000000 false true FE09938C-84BC-E75A-D767-84B85F48C4DB SAM Authentication Challenge for Security Dynamics mechanism 0 SecurID Passcode 2 Password error molequeue-0.9.0/molequeue/app/testing/data/compositeiodevice-ref/000077500000000000000000000000001323436134600251615ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/compositeiodevice-ref/testfile.txt000066400000000000000000000007621323436134600275460ustar00rootroot00000000000000kdsjflakjsdfl kasdjflka sdjflkj kfdsjlkasjfdlkjsadfkj lksajfdlksajfd lkjas lkfdj lkasjfd lkjsadf lkajsdflk jas ldfj ladsjf lksajdf lkjsafd lkjsadlkfjasdlkfj lksafdjlsajfd lka sjfd lajfdslk jasdlkfjsaldfjlksadjflkasjfdlkajsdlkfjasdjflsadfjlksadjflkasdjflkajdsflkahsdfkjhasjfhasfjsakdhf2oiuoirewquadhafhkjsadhfkjashfd kajshfdkjashfdkja shfdqwh roiuhdfuhqwerqhewourihdfslkjashfiuehrkjasdhfuahfdjahfiuewhafafroiewur poafdjoiwqeufoiq weufoias jd fwoie hjf ufoiew jfoiwaejfiowajfmwoiejfioweahnfjhasdfnl molequeue-0.9.0/molequeue/app/testing/data/dirlistinginfo-ref/000077500000000000000000000000001323436134600244735ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/dirlistinginfo-ref/dirlistinginfo.xml000066400000000000000000000017131323436134600302430ustar00rootroot00000000000000 /home/u/username 4096 . drwx------ Sep 19 10:51 username chl 4096 test drwx------ Sep 19 10:51 username chl 1705 .bash_history -rw------- Sep 6 16:44 username chl 1705 file -rw------- Sep 6 16:44 username chl molequeue-0.9.0/molequeue/app/testing/data/filespec-ref/000077500000000000000000000000001323436134600232415ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/filespec-ref/contents.json000066400000000000000000000001111323436134600257620ustar00rootroot00000000000000{ "contents": "I'm input file text!\n", "filename": "file.ext" } molequeue-0.9.0/molequeue/app/testing/data/filespec-ref/path.json000066400000000000000000000000531323436134600250660ustar00rootroot00000000000000{ "path": "/some/path/to/a/file.ext" } molequeue-0.9.0/molequeue/app/testing/data/filestreamingdata-ref/000077500000000000000000000000001323436134600251325ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/filestreamingdata-ref/filestreamingdata.xml000066400000000000000000000003071323436134600313370ustar00rootroot00000000000000 TOKENDATA /home/u/username/TESTFILE_UPLOAD 1 username molequeue-0.9.0/molequeue/app/testing/data/jobeventlist-ref/000077500000000000000000000000001323436134600241575ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/jobeventlist-ref/jobeventlist.xml000066400000000000000000000126041323436134600274140ustar00rootroot00000000000000 ruby.erdc.hpc.mil JOB_FINISH 1124393333 100535 4 1124393277 username biggiesmalls ruby.erdc.hpc.mil /Work/username/20050818_1427 STDOUT.TXT STDERR.TXT KeithLSTest erdcvenq 64 done 4 0 0 1124393284 ruby.erdc.hpc.mil JOB_FINISH 1124393333 100535 4 1124393277 username biggiesmalls ruby.erdc.hpc.mil /Work/username/20050818_1427 STDOUT.TXT STDERR.TXT KeithLSTest erdcvenq 64 done 4 0 0 1124393284 ruby.erdc.hpc.mil JOB_FINISH 1124393333 100536 4 1124393277 username biggiesmalls ruby.erdc.hpc.mil /Work/username/20050818_1427 STDOUT.TXT STDERR.TXT KeithLSTest erdcvenq 64 done 4 0 0 1124393284 ruby.erdc.hpc.mil JOB_FINISH 1124393333 100537 4 1124393277 username biggiesmalls ruby.erdc.hpc.mil /Work/username/20050818_1427 STDOUT.TXT STDERR.TXT KeithLSTest erdcvenq 64 done 4 0 0 1124393284 ruby.erdc.hpc.mil JOB_FINISH 1124393333 100539 4 1124393277 username2 biggiesmalls2 ruby.erdc.hpc.mil /Work/username/20050818_1427 STDOUT.TXT STDERR.TXT KeithLSTest erdcvenq 64 done 4 0 0 1124393284 ruby.erdc.hpc.mil JOB_FINISH 1124393333 100539 4 1124393277 username2 biggiesmalls2 ruby.erdc.hpc.mil /Work/username/20050818_1427 STDOUT.TXT STDERR.TXT KeithLSTest erdcvenq 64 done 4 0 0 1124393284 molequeue-0.9.0/molequeue/app/testing/data/jobsubmissioninfo-ref/000077500000000000000000000000001323436134600252115ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/jobsubmissioninfo-ref/jobsubmissioninfo.xml000066400000000000000000000002631323436134600314760ustar00rootroot00000000000000 343242.sdb Job <75899> is submitted to debug queue. error molequeue-0.9.0/molequeue/app/testing/data/jsonrpc-ref/000077500000000000000000000000001323436134600231255ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/jsonrpc-ref/internalPing-request.json000066400000000000000000000001051323436134600301340ustar00rootroot00000000000000{ "id": 24, "jsonrpc": "2.0", "method": "internalPing" } molequeue-0.9.0/molequeue/app/testing/data/jsonrpc-ref/internalPing-response.json000066400000000000000000000000751323436134600303100ustar00rootroot00000000000000{ "id": 24, "jsonrpc": "2.0", "result": "pong" } molequeue-0.9.0/molequeue/app/testing/data/kerberoscredentials-ref/000077500000000000000000000000001323436134600255015ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/kerberoscredentials-ref/kerberoscredentials.xml000066400000000000000000000001721323436134600322550ustar00rootroot00000000000000 test test molequeue-0.9.0/molequeue/app/testing/data/message-ref/000077500000000000000000000000001323436134600230735ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/message-ref/errorJson-arrayData.json000066400000000000000000000002541323436134600276600ustar00rootroot00000000000000{ "error": { "code": 666, "data": [ "Test" ], "message": "Server is possessed." }, "id": 13, "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/message-ref/errorJson-noData.json000066400000000000000000000001741323436134600271570ustar00rootroot00000000000000{ "error": { "code": 666, "message": "Server is possessed." }, "id": 13, "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/message-ref/errorJson-objectData.json000066400000000000000000000002651323436134600300120ustar00rootroot00000000000000{ "error": { "code": 666, "data": { "test": "value" }, "message": "Server is possessed." }, "id": 13, "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/message-ref/errorJson-primData.json000066400000000000000000000002201323436134600275020ustar00rootroot00000000000000{ "error": { "code": 666, "data": 55, "message": "Server is possessed." }, "id": 13, "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/message-ref/invalidJson.json000066400000000000000000000000041323436134600262400ustar00rootroot00000000000000{ } molequeue-0.9.0/molequeue/app/testing/data/message-ref/notificationJson-arrayParams.json000066400000000000000000000001251323436134600315640ustar00rootroot00000000000000{ "jsonrpc": "2.0", "method": "poke", "params": [ "Test" ] } molequeue-0.9.0/molequeue/app/testing/data/message-ref/notificationJson-noParams.json000066400000000000000000000000571323436134600310660ustar00rootroot00000000000000{ "jsonrpc": "2.0", "method": "poke" } molequeue-0.9.0/molequeue/app/testing/data/message-ref/notificationJson-objectParams.json000066400000000000000000000001361323436134600317160ustar00rootroot00000000000000{ "jsonrpc": "2.0", "method": "poke", "params": { "test": "value" } } molequeue-0.9.0/molequeue/app/testing/data/message-ref/requestJson-arrayParams.json000066400000000000000000000001501323436134600305640ustar00rootroot00000000000000{ "id": 1, "jsonrpc": "2.0", "method": "testMethod", "params": [ "Test" ] } molequeue-0.9.0/molequeue/app/testing/data/message-ref/requestJson-noParams.json000066400000000000000000000001021323436134600300570ustar00rootroot00000000000000{ "id": 1, "jsonrpc": "2.0", "method": "testMethod" } molequeue-0.9.0/molequeue/app/testing/data/message-ref/requestJson-objectParams.json000066400000000000000000000001611323436134600307160ustar00rootroot00000000000000{ "id": 1, "jsonrpc": "2.0", "method": "testMethod", "params": { "test": "value" } } molequeue-0.9.0/molequeue/app/testing/data/message-ref/responseJson.json000066400000000000000000000003451323436134600264600ustar00rootroot00000000000000{ "id": 42, "jsonrpc": "2.0", "result": [ null, { "test": "value" }, [ "Test" ], true, 5, 5.36893, "Abrakadabra" ] } molequeue-0.9.0/molequeue/app/testing/data/server-ref/000077500000000000000000000000001323436134600227555ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/server-ref/cancelJob-invalidQueue-request.json000066400000000000000000000001651323436134600316510ustar00rootroot00000000000000{ "id": 104, "jsonrpc": "2.0", "method": "cancelJob", "params": { "moleQueueId": 334 } } molequeue-0.9.0/molequeue/app/testing/data/server-ref/cancelJob-invalidQueue-response.json000066400000000000000000000003361323436134600320170ustar00rootroot00000000000000{ "error": { "code": 1, "data": { "moleQueueId": 334, "queue": "invalidQueue" }, "message": "Queue no longer exists" }, "id": 104, "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/cancelJob-invalidQueue/000077500000000000000000000000001323436134600272665ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/server-ref/cancelJob-invalidQueue/mqjobinfo.json000066400000000000000000000001651323436134600321470ustar00rootroot00000000000000{ "jobState": "QueuedLocal", "moleQueueId": 334, "program": "fakeProgram", "queue": "invalidQueue" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/cancelJob-jobNotRunning-request.json000066400000000000000000000001651323436134600320120ustar00rootroot00000000000000{ "id": 104, "jsonrpc": "2.0", "method": "cancelJob", "params": { "moleQueueId": 333 } } molequeue-0.9.0/molequeue/app/testing/data/server-ref/cancelJob-jobNotRunning-response.json000066400000000000000000000003521323436134600321560ustar00rootroot00000000000000{ "error": { "code": 4, "data": { "jobState": "Finished", "moleQueueId": 333 }, "message": "Cannot cancel job: Job not running." }, "id": 104, "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/cancelJob-jobNotRunning/000077500000000000000000000000001323436134600274275ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/server-ref/cancelJob-jobNotRunning/mqjobinfo.json000066400000000000000000000001571323436134600323110ustar00rootroot00000000000000{ "jobState": "Finished", "moleQueueId": 333, "program": "fakeProgram", "queue": "fakeQueue" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/cancelJob-moleQueueIdInvalid-request.json000066400000000000000000000001671323436134600327450ustar00rootroot00000000000000{ "id": 104, "jsonrpc": "2.0", "method": "cancelJob", "params": { "moleQueueId": 97868 } } molequeue-0.9.0/molequeue/app/testing/data/server-ref/cancelJob-moleQueueIdInvalid-response.json000066400000000000000000000002711323436134600331070ustar00rootroot00000000000000{ "error": { "code": 3, "data": { "moleQueueId": 97868 }, "message": "Unknown MoleQueue ID" }, "id": 104, "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/cancelJob-moleQueueIdMissing-request.json000066400000000000000000000001561323436134600327660ustar00rootroot00000000000000{ "id": 104, "jsonrpc": "2.0", "method": "cancelJob", "params": { "test": 5.3 } } molequeue-0.9.0/molequeue/app/testing/data/server-ref/cancelJob-moleQueueIdMissing-response.json000066400000000000000000000006721323436134600331370ustar00rootroot00000000000000{ "error": { "code": -32602, "data": { "description": "Required params.moleQueueId member missing.", "request": { "id": 104, "jsonrpc": "2.0", "method": "cancelJob", "params": { "test": 5.3 } } }, "message": "Invalid params" }, "id": 104, "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/cancelJob-paramsNotObject-request.json000066400000000000000000000001461323436134600323100ustar00rootroot00000000000000{ "id": 104, "jsonrpc": "2.0", "method": "cancelJob", "params": [ 5.3 ] } molequeue-0.9.0/molequeue/app/testing/data/server-ref/cancelJob-paramsNotObject-response.json000066400000000000000000000006611323436134600324600ustar00rootroot00000000000000{ "error": { "code": -32602, "data": { "description": "cancelJob params member must be an object.", "request": { "id": 104, "jsonrpc": "2.0", "method": "cancelJob", "params": [ 5.3 ] } }, "message": "Invalid params" }, "id": 104, "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/cancelJob-request.json000066400000000000000000000001651323436134600272200ustar00rootroot00000000000000{ "id": 104, "jsonrpc": "2.0", "method": "cancelJob", "params": { "moleQueueId": 335 } } molequeue-0.9.0/molequeue/app/testing/data/server-ref/cancelJob-response.json000066400000000000000000000001321323436134600273600ustar00rootroot00000000000000{ "id": 104, "jsonrpc": "2.0", "result": { "moleQueueId": 335 } } molequeue-0.9.0/molequeue/app/testing/data/server-ref/cancelJob/000077500000000000000000000000001323436134600246355ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/server-ref/cancelJob/mqjobinfo.json000066400000000000000000000001621323436134600275130ustar00rootroot00000000000000{ "jobState": "QueuedLocal", "moleQueueId": 335, "program": "fakeProgram", "queue": "fakeQueue" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/invalidMethod-request.json000066400000000000000000000001101323436134600301150ustar00rootroot00000000000000{ "id": 100, "jsonrpc": "2.0", "method": "notARealMethod" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/invalidMethod-response.json000066400000000000000000000004501323436134600302720ustar00rootroot00000000000000{ "error": { "code": -32601, "data": { "request": { "id": 100, "jsonrpc": "2.0", "method": "notARealMethod" } }, "message": "Method not found" }, "id": 100, "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/listOpenWithNames-request.json000066400000000000000000000001151323436134600307500ustar00rootroot00000000000000{ "jsonrpc": "2.0", "method": "listOpenWithNames", "id": "777" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/listOpenWithNames-response.json000066400000000000000000000001761323436134600311250ustar00rootroot00000000000000{ "id": "777", "jsonrpc": "2.0", "result": [ "My Spiffy Client", "My Spiffy Client (RPC)" ] } molequeue-0.9.0/molequeue/app/testing/data/server-ref/listQueues-request.json000066400000000000000000000001041323436134600274740ustar00rootroot00000000000000{ "id": 101, "jsonrpc": "2.0", "method": "listQueues" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/listQueues-response.json000066400000000000000000000003231323436134600276450ustar00rootroot00000000000000{ "id": 101, "jsonrpc": "2.0", "result": { "fakeQueue": [ "fakeProgram1", "fakeProgram2" ], "testQueue": [ "testProgram" ] } } molequeue-0.9.0/molequeue/app/testing/data/server-ref/lookupJob-moleQueueIdInvalid-request.json000066400000000000000000000001651323436134600330270ustar00rootroot00000000000000{ "id": 104, "jsonrpc": "2.0", "method": "lookupJob", "params": { "moleQueueId": 100 } } molequeue-0.9.0/molequeue/app/testing/data/server-ref/lookupJob-moleQueueIdInvalid-response.json000066400000000000000000000002671323436134600332000ustar00rootroot00000000000000{ "error": { "code": 3, "data": { "moleQueueId": 100 }, "message": "Unknown MoleQueue ID" }, "id": 104, "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/lookupJob-moleQueueIdMissing-request.json000066400000000000000000000001561323436134600330520ustar00rootroot00000000000000{ "id": 104, "jsonrpc": "2.0", "method": "lookupJob", "params": { "test": 333 } } molequeue-0.9.0/molequeue/app/testing/data/server-ref/lookupJob-moleQueueIdMissing-response.json000066400000000000000000000006721323436134600332230ustar00rootroot00000000000000{ "error": { "code": -32602, "data": { "description": "Required params.moleQueueId member missing.", "request": { "id": 104, "jsonrpc": "2.0", "method": "lookupJob", "params": { "test": 333 } } }, "message": "Invalid params" }, "id": 104, "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/lookupJob-paramsNotObject-request.json000066400000000000000000000001461323436134600323740ustar00rootroot00000000000000{ "id": 104, "jsonrpc": "2.0", "method": "lookupJob", "params": [ 5.3 ] } molequeue-0.9.0/molequeue/app/testing/data/server-ref/lookupJob-paramsNotObject-response.json000066400000000000000000000006611323436134600325440ustar00rootroot00000000000000{ "error": { "code": -32602, "data": { "description": "lookupJob params member must be an object.", "request": { "id": 104, "jsonrpc": "2.0", "method": "lookupJob", "params": [ 5.3 ] } }, "message": "Invalid params" }, "id": 104, "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/lookupJob-request.json000066400000000000000000000001651323436134600273040ustar00rootroot00000000000000{ "id": 104, "jsonrpc": "2.0", "method": "lookupJob", "params": { "moleQueueId": 777 } } molequeue-0.9.0/molequeue/app/testing/data/server-ref/lookupJob-response.json000066400000000000000000000011771323436134600274560ustar00rootroot00000000000000{ "id": 104, "jsonrpc": "2.0", "result": { "cleanLocalWorkingDirectory": false, "cleanRemoteFiles": false, "description": "Some job", "hideFromGui": false, "inputFile": { }, "jobState": "QueuedLocal", "localWorkingDirectory": "/working/directory/jobs/777", "maxWallTime": -1, "moleQueueId": 777, "numberOfCores": 8, "outputDirectory": "/working/directory/jobs/777", "popupOnStateChange": false, "program": "fakeProgram", "queue": "fakeQueue", "queueId": null, "retrieveOutput": true } } molequeue-0.9.0/molequeue/app/testing/data/server-ref/lookupJob/000077500000000000000000000000001323436134600247215ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/server-ref/lookupJob/mqjobinfo.json000066400000000000000000000010001323436134600275670ustar00rootroot00000000000000{ "cleanLocalWorkingDirectory": false, "cleanRemoteFiles": false, "description": "Some job", "hideFromGui": false, "inputFile": { }, "jobState": "QueuedLocal", "localWorkingDirectory": "/working/directory/jobs/777", "maxWallTime": -1, "moleQueueId": 777, "numberOfCores": 8, "outputDirectory": "/working/directory/jobs/777", "popupOnStateChange": false, "program": "fakeProgram", "queue": "fakeQueue", "queueId": null, "retrieveOutput": true } molequeue-0.9.0/molequeue/app/testing/data/server-ref/registerOpenWith-badNameExec-request.json000066400000000000000000000005271323436134600327760ustar00rootroot00000000000000{ "jsonrpc": "2.0", "method": "registerOpenWith", "params": { "name": 32, "patterns": [ { "regexp": "spiff[\\d]*\\.(?:dat|out)", "caseSensitive": true }, { "wildcard": "*.spiffyout" } ] }, "id": "777" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/registerOpenWith-badNameExec-response.json000066400000000000000000000014631323436134600331440ustar00rootroot00000000000000{ "error": { "code": -32602, "data": { "description": "\"params.name\" (string) and \"params.method\" (object) must both be present.", "request": { "id": "777", "jsonrpc": "2.0", "method": "registerOpenWith", "params": { "name": 32, "patterns": [ { "caseSensitive": true, "regexp": "spiff[\\d]*\\.(?:dat|out)" }, { "wildcard": "*.spiffyout" } ] } } }, "message": "Invalid params" }, "id": "777", "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/registerOpenWith-duplicateName-request.json000066400000000000000000000006511323436134600334130ustar00rootroot00000000000000{ "jsonrpc": "2.0", "method": "registerOpenWith", "params": { "name": "My Spiffy Client", "method": { "executable": "client" }, "patterns": [ { "regexp": "spiff[\\d]*\\.(?:dat|out)", "caseSensitive": true }, { "wildcard": "*.spiffyout" } ] }, "id": "777" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/registerOpenWith-duplicateName-response.json000066400000000000000000000002651323436134600335620ustar00rootroot00000000000000{ "error": { "code": 1, "message": "Name conflict: An open-with handler named 'My Spiffy Client' already exists." }, "id": "777", "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/registerOpenWith-emptyName-request.json000066400000000000000000000006311323436134600325750ustar00rootroot00000000000000{ "jsonrpc": "2.0", "method": "registerOpenWith", "params": { "name": "", "method": { "executable": "client" }, "patterns": [ { "regexp": "spiff[\\d]*\\.(?:dat|out)", "caseSensitive": true }, { "wildcard": "*.spiffyout" } ] }, "id": "777" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/registerOpenWith-emptyName-response.json000066400000000000000000000015671323436134600327540ustar00rootroot00000000000000{ "error": { "code": -32602, "data": { "description": "\"params.name\" must be a non-empty string.", "request": { "id": "777", "jsonrpc": "2.0", "method": "registerOpenWith", "params": { "method": { "executable": "client" }, "name": "", "patterns": [ { "caseSensitive": true, "regexp": "spiff[\\d]*\\.(?:dat|out)" }, { "wildcard": "*.spiffyout" } ] } } }, "message": "Invalid params" }, "id": "777", "jsonrpc": "2.0" } registerOpenWith-invalidPatternType-request.json000066400000000000000000000005431323436134600344070ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/server-ref{ "jsonrpc": "2.0", "method": "registerOpenWith", "params": { "name": "My Spiffy Client", "method": { "executable": "client" }, "patterns": [ { "invalid": "spiff[\\d]*\\.(?:dat|out)", "caseSensitive": true } ] }, "id": "777" } registerOpenWith-invalidPatternType-response.json000066400000000000000000000014711323436134600345560ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/server-ref{ "error": { "code": -32602, "data": { "description": "\"params.patterns\" contains an entry that is not a regexp or wildcard.", "request": { "id": "777", "jsonrpc": "2.0", "method": "registerOpenWith", "params": { "method": { "executable": "client" }, "name": "My Spiffy Client", "patterns": [ { "caseSensitive": true, "invalid": "spiff[\\d]*\\.(?:dat|out)" } ] } } }, "message": "Invalid params" }, "id": "777", "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/registerOpenWith-paramsNotObject-request.json000066400000000000000000000001711323436134600337300ustar00rootroot00000000000000{ "jsonrpc": "2.0", "method": "registerOpenWith", "params": ["I'm not an object at all!"], "id": "777" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/registerOpenWith-paramsNotObject-response.json000066400000000000000000000007331323436134600341020ustar00rootroot00000000000000{ "error": { "code": -32602, "data": { "description": "registerOpenWith params member must be an object.", "request": { "id": "777", "jsonrpc": "2.0", "method": "registerOpenWith", "params": [ "I'm not an object at all!" ] } }, "message": "Invalid params" }, "id": "777", "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/registerOpenWith-patternNotObject-request.json000066400000000000000000000006711323436134600341270ustar00rootroot00000000000000{ "jsonrpc": "2.0", "method": "registerOpenWith", "params": { "name": "My Spiffy Client", "method": { "executable": "client" }, "patterns": [ { "regexp": "spiff[\\d]*\\.(?:dat|out)", "caseSensitive": true }, { "wildcard": "*.spiffyout" }, 12 ] }, "id": "777" } registerOpenWith-patternNotObject-response.json000066400000000000000000000016571323436134600342230ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/server-ref{ "error": { "code": -32602, "data": { "description": "\"params.patterns\" array entries must be JSON objects.", "request": { "id": "777", "jsonrpc": "2.0", "method": "registerOpenWith", "params": { "method": { "executable": "client" }, "name": "My Spiffy Client", "patterns": [ { "caseSensitive": true, "regexp": "spiff[\\d]*\\.(?:dat|out)" }, { "wildcard": "*.spiffyout" }, 12 ] } } }, "message": "Invalid params" }, "id": "777", "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/registerOpenWith-patternsNotArray-request.json000066400000000000000000000004021323436134600341520ustar00rootroot00000000000000{ "id": "777", "jsonrpc": "2.0", "method": "registerOpenWith", "params": { "method": { "executable": "client" }, "name": "My Spiffy Client", "patterns": { "derp": true } } } registerOpenWith-patternsNotArray-response.json000066400000000000000000000012351323436134600342460ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/server-ref{ "error": { "code": -32602, "data": { "description": "\"params.patterns\" member must be a JSON array.", "request": { "id": "777", "jsonrpc": "2.0", "method": "registerOpenWith", "params": { "method": { "executable": "client" }, "name": "My Spiffy Client", "patterns": { "derp": true } } } }, "message": "Invalid params" }, "id": "777", "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/registerOpenWith-request.json000066400000000000000000000006521323436134600306430ustar00rootroot00000000000000{ "jsonrpc": "2.0", "method": "registerOpenWith", "params": { "name": "My Spiffy Client", "method": { "executable": "client" }, "patterns": [ { "regexp": "spiff[\\d]*\\.(?:dat|out)" }, { "wildcard": "*.spiffyout", "caseSensitive": false } ] }, "id": "777" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/registerOpenWith-response.json000066400000000000000000000001031323436134600310000ustar00rootroot00000000000000{ "id": "777", "jsonrpc": "2.0", "result": "success" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/registerOpenWith-rpc-request.json000066400000000000000000000007361323436134600314300ustar00rootroot00000000000000{ "jsonrpc": "2.0", "method": "registerOpenWith", "params": { "name": "My Spiffy Client (RPC)", "method": { "rpcServer": "rpc-client", "rpcMethod": "readFile" }, "patterns": [ { "regexp": "rpcspiff[\\d]*\\.(?:dat|out)" }, { "wildcard": "rpc*.spiffyout", "caseSensitive": false } ] }, "id": "777" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/registerOpenWith-rpc-response.json000066400000000000000000000001031323436134600315620ustar00rootroot00000000000000{ "id": "777", "jsonrpc": "2.0", "result": "success" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/submitJob-paramsNotObject-request.json000066400000000000000000000001461323436134600323660ustar00rootroot00000000000000{ "id": 102, "jsonrpc": "2.0", "method": "submitJob", "params": [ 5.3 ] } molequeue-0.9.0/molequeue/app/testing/data/server-ref/submitJob-paramsNotObject-response.json000066400000000000000000000006611323436134600325360ustar00rootroot00000000000000{ "error": { "code": -32602, "data": { "description": "submitJob params member must be an object.", "request": { "id": 102, "jsonrpc": "2.0", "method": "submitJob", "params": [ 5.3 ] } }, "message": "Invalid params" }, "id": 102, "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/submitJob-programDoesNotExist-request.json000066400000000000000000000003031323436134600332460ustar00rootroot00000000000000{ "id": 103, "jsonrpc": "2.0", "method": "submitJob", "params": { "description": "testDescription", "program": "testProgram", "queue": "fakeQueue" } } molequeue-0.9.0/molequeue/app/testing/data/server-ref/submitJob-programDoesNotExist-response.json000066400000000000000000000011671323436134600334250ustar00rootroot00000000000000{ "error": { "code": 2, "data": { "program": "testProgram", "request": { "id": 103, "jsonrpc": "2.0", "method": "submitJob", "params": { "description": "testDescription", "program": "testProgram", "queue": "fakeQueue" } }, "valid programs for queue": [ "fakeProgram1", "fakeProgram2" ] }, "message": "Invalid program" }, "id": 103, "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/submitJob-programMissing-request.json000066400000000000000000000002411323436134600322700ustar00rootroot00000000000000{ "id": 103, "jsonrpc": "2.0", "method": "submitJob", "params": { "description": "testDescription", "queue": "testQueue" } } molequeue-0.9.0/molequeue/app/testing/data/server-ref/submitJob-programMissing-response.json000066400000000000000000000007651323436134600324510ustar00rootroot00000000000000{ "error": { "code": -32602, "data": { "description": "Required params.program member missing.", "request": { "id": 103, "jsonrpc": "2.0", "method": "submitJob", "params": { "description": "testDescription", "queue": "testQueue" } } }, "message": "Invalid params" }, "id": 103, "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/submitJob-programNotString-request.json000066400000000000000000000002721323436134600326120ustar00rootroot00000000000000{ "id": 103, "jsonrpc": "2.0", "method": "submitJob", "params": { "description": "testDescription", "program": 68.9, "queue": "testQueue" } } molequeue-0.9.0/molequeue/app/testing/data/server-ref/submitJob-programNotString-response.json000066400000000000000000000010321323436134600327530ustar00rootroot00000000000000{ "error": { "code": -32602, "data": { "description": "params.program member must be a string.", "request": { "id": 103, "jsonrpc": "2.0", "method": "submitJob", "params": { "description": "testDescription", "program": 68.9, "queue": "testQueue" } } }, "message": "Invalid params" }, "id": 103, "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/submitJob-queueDoesNotExist-request.json000066400000000000000000000003061323436134600327260ustar00rootroot00000000000000{ "id": 103, "jsonrpc": "2.0", "method": "submitJob", "params": { "description": "testDescription", "program": "testProgram", "queue": "invalidQueue" } } molequeue-0.9.0/molequeue/app/testing/data/server-ref/submitJob-queueDoesNotExist-response.json000066400000000000000000000011451323436134600330760ustar00rootroot00000000000000{ "error": { "code": 1, "data": { "queue": "invalidQueue", "request": { "id": 103, "jsonrpc": "2.0", "method": "submitJob", "params": { "description": "testDescription", "program": "testProgram", "queue": "invalidQueue" } }, "valid queues": [ "fakeQueue", "testQueue" ] }, "message": "Invalid queue" }, "id": 103, "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/submitJob-queueMissing-request.json000066400000000000000000000002451323436134600317510ustar00rootroot00000000000000{ "id": 103, "jsonrpc": "2.0", "method": "submitJob", "params": { "description": "testDescription", "program": "testProgram" } } molequeue-0.9.0/molequeue/app/testing/data/server-ref/submitJob-queueMissing-response.json000066400000000000000000000007671323436134600321300ustar00rootroot00000000000000{ "error": { "code": -32602, "data": { "description": "Required params.queue member missing.", "request": { "id": 103, "jsonrpc": "2.0", "method": "submitJob", "params": { "description": "testDescription", "program": "testProgram" } } }, "message": "Invalid params" }, "id": 103, "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/submitJob-queueNotString-request.json000066400000000000000000000002741323436134600322710ustar00rootroot00000000000000{ "id": 103, "jsonrpc": "2.0", "method": "submitJob", "params": { "description": "testDescription", "program": "testProgram", "queue": 53.2 } } molequeue-0.9.0/molequeue/app/testing/data/server-ref/submitJob-queueNotString-response.json000066400000000000000000000010321323436134600324300ustar00rootroot00000000000000{ "error": { "code": -32602, "data": { "description": "params.queue member must be a string.", "request": { "id": 103, "jsonrpc": "2.0", "method": "submitJob", "params": { "description": "testDescription", "program": "testProgram", "queue": 53.2 } } }, "message": "Invalid params" }, "id": 103, "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/submitJob-request.json000066400000000000000000000003031323436134600272700ustar00rootroot00000000000000{ "id": 103, "jsonrpc": "2.0", "method": "submitJob", "params": { "description": "testDescription", "program": "testProgram", "queue": "testQueue" } } molequeue-0.9.0/molequeue/app/testing/data/server-ref/submitJob-response.json000066400000000000000000000001771323436134600274470ustar00rootroot00000000000000{ "id": 103, "jsonrpc": "2.0", "result": { "moleQueueId": 1, "workingDirectory": "/jobs/1" } } molequeue-0.9.0/molequeue/app/testing/data/server-ref/unregisterOpenWith-nameNotString-request.json000066400000000000000000000001731323436134600337720ustar00rootroot00000000000000{ "jsonrpc": "2.0", "method": "unregisterOpenWith", "params": { "name": false }, "id": "780" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/unregisterOpenWith-nameNotString-response.json000066400000000000000000000007051323436134600341410ustar00rootroot00000000000000{ "error": { "code": -32602, "data": { "description": "\"params.name\" value must be a string.", "request": { "id": "780", "jsonrpc": "2.0", "method": "unregisterOpenWith", "params": { "name": false } } }, "message": "Invalid params" }, "id": "780", "jsonrpc": "2.0" } unregisterOpenWith-paramsNotObject-request.json000066400000000000000000000001761323436134600342210ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/server-ref{ "jsonrpc": "2.0", "method": "unregisterOpenWith", "params": ["Badly specified handler name"], "id": "780" } unregisterOpenWith-paramsNotObject-response.json000066400000000000000000000007161323436134600343670ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/server-ref{ "error": { "code": -32602, "data": { "description": "params value must be an object.", "request": { "id": "780", "jsonrpc": "2.0", "method": "unregisterOpenWith", "params": [ "Badly specified handler name" ] } }, "message": "Invalid params" }, "id": "780", "jsonrpc": "2.0" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/unregisterOpenWith-prepare-request.json000066400000000000000000000003001323436134600326300ustar00rootroot00000000000000{ "jsonrpc": "2.0", "method": "registerOpenWith", "params": { "name": "Remove me!", "method": { "executable": "exec" } }, "id": "779" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/unregisterOpenWith-prepare-response.json000066400000000000000000000001031323436134600327770ustar00rootroot00000000000000{ "id": "779", "jsonrpc": "2.0", "result": "success" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/unregisterOpenWith-request.json000066400000000000000000000002021323436134600311750ustar00rootroot00000000000000{ "jsonrpc": "2.0", "method": "unregisterOpenWith", "params": { "name": "Remove me!" }, "id": "780" } molequeue-0.9.0/molequeue/app/testing/data/server-ref/unregisterOpenWith-response.json000066400000000000000000000001031323436134600313430ustar00rootroot00000000000000{ "id": "780", "jsonrpc": "2.0", "result": "success" } molequeue-0.9.0/molequeue/app/testing/data/testworkdir_unix/000077500000000000000000000000001323436134600243215ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/testworkdir_unix/config/000077500000000000000000000000001323436134600255665ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/testworkdir_unix/config/queues/000077500000000000000000000000001323436134600270755ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/testworkdir_unix/config/queues/TestQueue.mqq000066400000000000000000000007201323436134600315400ustar00rootroot00000000000000{ "cores" : -1, "jobIdMap" : {}, "jobsToResume" : [], "launchScriptName" : "MoleQueueLauncher.sh", "launchTemplate" : "#!/bin/bash\n\n$$programExecution$$\n", "programs" : { "TestProgram" : { "arguments" : "empty.txt", "customLaunchTemplate" : "", "executable" : "touch", "inputFilename" : "job.inp", "launchSyntax" : 1, "outputFilename" : "job.out" } }, "type" : "Local" } molequeue-0.9.0/molequeue/app/testing/data/uit-ref/000077500000000000000000000000001323436134600222505ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/uit-ref/jobeventlist.xml000066400000000000000000000071531323436134600255100ustar00rootroot00000000000000 ruby.erdc.hpc.mil JOB_FINISH 1124393333 100535 4 1124393277 username biggiesmalls ruby.erdc.hpc.mil /Work/username/20050818_1427 STDOUT.TXT STDERR.TXT KeithLSTest erdcvenq g done 4 0 0 1124393284 ruby.erdc.hpc.mil JOB_FINISH 1124393334 100535 4 1124393277 username biggiesmalls ruby.erdc.hpc.mil /Work/username/20050818_1427 STDOUT.TXT STDERR.TXT KeithLSTest erdcvenq Q done 4 0 0 1124393284 ruby.erdc.hpc.mil JOB_FINISH 2124393333 100536 4 91243932600 username biggiesmalls ruby.erdc.hpc.mil /Work/username/20050818_1427 STDOUT.TXT STDERR.TXT KeithLSTest erdcvenq q done 4 0 0 1124393284 ruby.erdc.hpc.mil JOB_FINISH 2124393334 100536 4 1124393277 username biggiesmalls ruby.erdc.hpc.mil /Work/username/20050818_1427 STDOUT.TXT STDERR.TXT KeithLSTest erdcvenq r done 4 0 0 1124393284 molequeue-0.9.0/molequeue/app/testing/data/userhostassoclist-ref/000077500000000000000000000000001323436134600252505ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/data/userhostassoclist-ref/userhostassoclisttest.xml000066400000000000000000000011021323436134600324650ustar00rootroot00000000000000 user 2 username ruby.erdc.hpc.mil ERDC::DIAMOND Origin 3900 MSRC_KERBEROS user 3 lead.erdc.hpc.mil ERDC::DIAMOND Origin 3901 MSRC_KERBEROS molequeue-0.9.0/molequeue/app/testing/dirlistinginfotest.cpp000066400000000000000000000050641323436134600244210ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include #include "queues/uit/dirlistinginfo.h" #include "referencestring.h" #include "xmlutils.h" class DirListingInfoTest : public QObject { Q_OBJECT private slots: void initTestCase(); void testFromXml(); private: QString m_dirListingInfo; }; void DirListingInfoTest::initTestCase() { m_dirListingInfo = ReferenceString("dirlistinginfo-ref/dirlistinginfo.xml"); } void DirListingInfoTest::testFromXml() { MoleQueue::Uit::DirListingInfo list = MoleQueue::Uit::DirListingInfo::fromXml(m_dirListingInfo); QCOMPARE(list.directories().size(), 2); QCOMPARE(list.files().size(), 2); QCOMPARE(list.currentDirectory(), QString("/home/u/username")); MoleQueue::Uit::FileInfo info = list.directories()[0]; QVERIFY(info.size() == 4096); QCOMPARE(info.name(), QString(".")); QCOMPARE(info.perms(), QString("drwx------")); QCOMPARE(info.date(), QString("Sep 19 10:51")); QCOMPARE(info.user(), QString("username")); QCOMPARE(info.group(), QString("chl")); info = list.directories()[1]; QVERIFY(info.size() == 4096); QCOMPARE(info.name(), QString("test")); QCOMPARE(info.perms(), QString("drwx------")); QCOMPARE(info.date(), QString("Sep 19 10:51")); QCOMPARE(info.user(), QString("username")); QCOMPARE(info.group(), QString("chl")); info = list.files()[0]; QVERIFY(info.size() == 1705); QCOMPARE(info.name(), QString(".bash_history")); QCOMPARE(info.perms(), QString("-rw-------")); QCOMPARE(info.date(), QString("Sep 6 16:44")); QCOMPARE(info.user(), QString("username")); QCOMPARE(info.group(), QString("chl")); info = list.files()[1]; QVERIFY(info.size() == 1705); QCOMPARE(info.name(), QString("file")); QCOMPARE(info.perms(), QString("-rw-------")); QCOMPARE(info.date(), QString("Sep 6 16:44")); QCOMPARE(info.user(), QString("username")); QCOMPARE(info.group(), QString("chl")); } QTEST_MAIN(DirListingInfoTest) #include "dirlistinginfotest.moc" molequeue-0.9.0/molequeue/app/testing/dummyconnection.cpp000066400000000000000000000031021323436134600236770ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "dummyconnection.h" #include #include DummyConnection::DummyConnection(QObject *parent_) : MoleQueue::Connection(parent_) { } void DummyConnection::emitPacketReceived(const MoleQueue::Message &message) { emit packetReceived(MoleQueue::PacketType(message.toJson()), MoleQueue::EndpointIdType()); } void DummyConnection::open() { } void DummyConnection::start() { } void DummyConnection::close() { } bool DummyConnection::isOpen() { return true; } QString DummyConnection::connectionString() const { return ""; } bool DummyConnection::send(const MoleQueue::PacketType &packet, const MoleQueue::EndpointIdType &endpoint) { MoleQueue::Message message( QJsonDocument::fromJson(QByteArray(packet)).object(), this, endpoint); message.parse(); m_messageQueue.append(message); return true; } void DummyConnection::flush() { } molequeue-0.9.0/molequeue/app/testing/dummyconnection.h000066400000000000000000000030201323436134600233430ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_DUMMYCONNECTION_H #define MOLEQUEUE_DUMMYCONNECTION_H #include #include class DummyConnection : public MoleQueue::Connection { Q_OBJECT public: explicit DummyConnection(QObject *parent_ = 0); void emitPacketReceived(const MoleQueue::Message &message); int messageCount() { return m_messageQueue.size(); } MoleQueue::Message popMessage() { if (!m_messageQueue.isEmpty()) return m_messageQueue.takeFirst(); return MoleQueue::Message(); } // Reimplemented from base class: void open(); void start(); void close(); bool isOpen(); QString connectionString() const; bool send(const MoleQueue::PacketType &packet, const MoleQueue::EndpointIdType &endpoint); void flush(); QList m_messageQueue; }; #endif // MOLEQUEUE_DUMMYCONNECTION_H molequeue-0.9.0/molequeue/app/testing/dummyconnectionlistener.cpp000066400000000000000000000021561323436134600254550ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "dummyconnectionlistener.h" #include "dummyconnection.h" DummyConnectionListener::DummyConnectionListener(QObject *aparent) : MoleQueue::ConnectionListener(aparent) { } void DummyConnectionListener::emitNewConnection(DummyConnection *conn) { emit newConnection(conn); } void DummyConnectionListener::start() { } void DummyConnectionListener::stop(bool) { } void DummyConnectionListener::stop() { } QString DummyConnectionListener::connectionString() const { return ""; } molequeue-0.9.0/molequeue/app/testing/dummyconnectionlistener.h000066400000000000000000000023211323436134600251140ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_DUMMYCONNECTIONLISTENER_H #define MOLEQUEUE_DUMMYCONNECTIONLISTENER_H #include class DummyConnection; class DummyConnectionListener : public MoleQueue::ConnectionListener { Q_OBJECT public: explicit DummyConnectionListener(QObject *aparent = 0); /// Emit @a conn as a new connection from this listener. void emitNewConnection(DummyConnection *conn); // Reimplemented from base class void start(); void stop(bool force); void stop(); QString connectionString() const; }; #endif // DUMMYCONNECTIONLISTENER_H molequeue-0.9.0/molequeue/app/testing/dummyqueuemanager.cpp000066400000000000000000000026201323436134600242230ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "dummyqueuemanager.h" #include "dummyqueueremote.h" using namespace MoleQueue; DummyQueueManager::DummyQueueManager(Server *parentServer) : MoleQueue::QueueManager(parentServer) { } DummyQueueManager::~DummyQueueManager() { } Queue *DummyQueueManager::addQueue(const QString &queueName, const QString &queueType, bool replace) { if (m_queues.contains(queueName)) { if (replace == true) m_queues.take(queueName)->deleteLater(); else return NULL; } Queue * newQueue = NULL; if (queueType == "Dummy") newQueue = new DummyQueueRemote(queueName, this); if (!newQueue) return NULL; m_queues.insert(newQueue->name(), newQueue); emit queueAdded(newQueue->name(), newQueue); return newQueue; } molequeue-0.9.0/molequeue/app/testing/dummyqueuemanager.h000066400000000000000000000020351323436134600236700ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef DUMMYQUEUEMANAGER_H #define DUMMYQUEUEMANAGER_H #include "queuemanager.h" class DummyQueueManager : public MoleQueue::QueueManager { Q_OBJECT public: DummyQueueManager(MoleQueue::Server *parentServer); virtual ~DummyQueueManager(); MoleQueue::Queue * addQueue(const QString &queueName, const QString &queueType, bool replace); }; #endif // DUMMYQUEUEMANAGER_H molequeue-0.9.0/molequeue/app/testing/dummyqueueremote.cpp000066400000000000000000000032131323436134600241030ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "dummyqueueremote.h" using namespace MoleQueue; DummyQueueRemote::DummyQueueRemote(const QString &queueName, QueueManager *parentObject) : MoleQueue::QueueRemoteSsh(queueName, parentObject), m_dummySsh(NULL) { m_launchScriptName = "launcher.dummy"; m_launchTemplate = "Run job $$moleQueueId$$!!"; } DummyQueueRemote::~DummyQueueRemote() { if (!m_dummySsh.isNull()) m_dummySsh->deleteLater(); } bool DummyQueueRemote::parseQueueId(const QString &submissionOutput, IdType *queueId) { Q_UNUSED(submissionOutput); *queueId = 12; return true; } bool DummyQueueRemote::parseQueueLine(const QString &queueListOutput, IdType *queueId, JobState *state) { // Output is "[queueId] [stateAsString]" QStringList split = queueListOutput.split(QRegExp("\\s+")); if (split.size() < 2) return false; *queueId = toIdType(split.at(0)); *state = stringToJobState(split.at(1)); return true; } molequeue-0.9.0/molequeue/app/testing/dummyqueueremote.h000066400000000000000000000035301323436134600235520ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef DUMMYQUEUEREMOTE_H #define DUMMYQUEUEREMOTE_H #include "queues/remotessh.h" #include "dummysshcommand.h" #include class QueueRemoteTest; class DummyQueueRemote : public MoleQueue::QueueRemoteSsh { Q_OBJECT public: DummyQueueRemote(const QString &queueName, MoleQueue::QueueManager *parentObject); ~DummyQueueRemote(); QString typeName() const { return "Dummy"; } DummySshCommand *getDummySshCommand() { return m_dummySsh.data(); } friend class QueueRemoteTest; protected: MoleQueue::SshConnection *newSshConnection() { if (!m_dummySsh.isNull()) { m_dummySsh->deleteLater(); m_dummySsh = NULL; } m_dummySsh = new DummySshCommand(); m_dummySsh->setHostName(m_hostName); m_dummySsh->setUserName(m_userName); m_dummySsh->setPortNumber(m_sshPort); m_dummySsh->setParent(this); return m_dummySsh.data(); } bool parseQueueId(const QString &submissionOutput, MoleQueue::IdType *queueId); bool parseQueueLine(const QString &queueListOutput, MoleQueue::IdType *queueId, MoleQueue::JobState *state); QPointer m_dummySsh; }; #endif // DUMMYQUEUEREMOTE_H molequeue-0.9.0/molequeue/app/testing/dummyserver.cpp000066400000000000000000000017231323436134600230550ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "dummyserver.h" #include DummyServer::DummyServer(QObject *parentObject) : Server(parentObject, getRandomSocketName()) { m_queueManager->deleteLater(); m_queueManager = new DummyQueueManager (this); m_workingDirectoryBase = QDir::tempPath() + "/MoleQueue-dummyServer/"; } DummyServer::~DummyServer() { } molequeue-0.9.0/molequeue/app/testing/dummyserver.h000066400000000000000000000033031323436134600225160ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef DUMMYSERVER_H #define DUMMYSERVER_H #include "server.h" #include "dummyqueuemanager.h" #include "server.h" #include #include #include #include /// "Safe" server to use in unit tests. Uses randomized socket name. class DummyServer : public MoleQueue::Server { Q_OBJECT public: DummyServer(QObject *parentObject = NULL); ~DummyServer(); static QString getRandomSocketName() { // Generate a time, process, and thread independent random value. quint32 threadPtr = static_cast( reinterpret_cast(QThread::currentThread())); quint32 procId = static_cast(qApp->applicationPid()); quint32 msecs = static_cast( QDateTime::currentDateTime().toMSecsSinceEpoch()); unsigned int seed = static_cast( (threadPtr ^ procId) ^ ((msecs << 16) ^ msecs)); qsrand(seed); int randVal = qrand(); return QString("MoleQueue-testing-%1").arg(QString::number(randVal)); } }; #endif // DUMMYSERVER_H molequeue-0.9.0/molequeue/app/testing/dummysshcommand.cpp000066400000000000000000000015141323436134600237010ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "dummysshcommand.h" using namespace MoleQueue; DummySshCommand::DummySshCommand(QObject *parentObject) : MoleQueue::OpenSshCommand(parentObject) { } DummySshCommand::~DummySshCommand() { } molequeue-0.9.0/molequeue/app/testing/dummysshcommand.h000066400000000000000000000027071323436134600233530ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef DUMMYSSHCOMMAND_H #define DUMMYSSHCOMMAND_H #include "opensshcommand.h" /// SshCommand implementation that doesn't actually call external processes. class DummySshCommand : public MoleQueue::OpenSshCommand { Q_OBJECT public: DummySshCommand(QObject *parentObject = NULL); ~DummySshCommand(); QString getDummyCommand() const { return m_dummyCommand; } QStringList getDummyArgs() const { return m_dummyArgs; } void setDummyOutput(const QString &out) { m_output = out; } void setDummyExitCode(int code) {m_exitCode = code; } void emitDummyRequestComplete() { emit requestComplete(); } protected: void sendRequest(const QString &command, const QStringList &args) { m_dummyCommand = command; m_dummyArgs = args; } QString m_dummyCommand; QStringList m_dummyArgs; }; #endif // DUMMYSSHCOMMAND_H molequeue-0.9.0/molequeue/app/testing/filespecificationtest.cpp000066400000000000000000000220211323436134600250450ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include "filespecification.h" #include "molequeuetestconfig.h" #include #include using namespace MoleQueue; class FileSpecificationTest : public QObject { Q_OBJECT private: QString readReferenceString(const QString &filename_); private slots: /// Called before the first test function is executed. void initTestCase(); /// Called after the last test function is executed. void cleanupTestCase(); /// Called before each test function is executed. void init(); /// Called after every test function. void cleanup(); void ctorFromJsonObject(); void ctorFromPath(); void ctorFromFileNameAndContents(); void ctorFromFile(); void ctorCopy(); void assignment(); void format(); void isValid(); void toJsonObject(); void fileExists(); void writeFile(); void filename(); void contents(); void filepath(); void fileHasExtension(); void fileBaseName(); void fileExtension(); }; QString FileSpecificationTest::readReferenceString(const QString &filename_) { QString realFilename = MoleQueue_TESTDATA_DIR + filename_; QFile refFile (realFilename); if (!refFile.open(QFile::ReadOnly | QIODevice::Text)) { qDebug() << "Cannot access reference file" << realFilename; return ""; } QString result = QString(refFile.readAll()); refFile.close(); return result; } void FileSpecificationTest::initTestCase() { } void FileSpecificationTest::cleanupTestCase() { } void FileSpecificationTest::init() { } void FileSpecificationTest::cleanup() { } void FileSpecificationTest::ctorFromJsonObject() { QJsonObject json; json.insert("path", QString("/some/path/to/a/file.ext")); FileSpecification pathSpec(json); QString ref = readReferenceString("filespec-ref/path.json"); QCOMPARE(QString(pathSpec.toJson()), ref); } void FileSpecificationTest::ctorFromPath() { FileSpecification pathSpec(QString("/some/path/to/a/file.ext")); QString ref = readReferenceString("filespec-ref/path.json"); QCOMPARE(QString(pathSpec.toJson()), ref); } void FileSpecificationTest::ctorFromFileNameAndContents() { FileSpecification contSpec(QString("file.ext"), QString("I'm input file text!\n")); QString ref = readReferenceString("filespec-ref/contents.json"); QCOMPARE(QString(contSpec.toJson()), ref); } void FileSpecificationTest::ctorFromFile() { QTemporaryFile file; QVERIFY(file.open()); file.setTextModeEnabled(true); QByteArray content("I'm input file text!!\n"); file.write(content); file.close(); FileSpecification spec(&file, FileSpecification::PathFileSpecification); QCOMPARE(spec.format(), FileSpecification::PathFileSpecification); QCOMPARE(spec.filepath(), QFileInfo(file).absoluteFilePath()); spec = FileSpecification(&file, FileSpecification::ContentsFileSpecification); QCOMPARE(spec.format(), FileSpecification::ContentsFileSpecification); QCOMPARE(spec.filename(), QFileInfo(file).fileName()); QCOMPARE(spec.contents(), QString(content)); } void FileSpecificationTest::ctorCopy() { FileSpecification spec1(QString("/path/to/some/file.ext")); FileSpecification spec2(spec1); QCOMPARE(spec1.toJson(), spec2.toJson()); } void FileSpecificationTest::assignment() { FileSpecification spec1(QString("/path/to/some/file.ext")); FileSpecification spec2; spec2 = spec1; QCOMPARE(spec1.toJson(), spec2.toJson()); } void FileSpecificationTest::format() { FileSpecification pathSpec(QString("/some/path/to/a/file.ext")); QCOMPARE(pathSpec.format(), FileSpecification::PathFileSpecification); FileSpecification contSpec(QString("file.ext"), QString("I'm input file text!\n")); QCOMPARE(contSpec.format(), FileSpecification::ContentsFileSpecification); QJsonObject json; FileSpecification inv1(json); QCOMPARE(inv1.format(), FileSpecification::InvalidFileSpecification); json.insert("notARealKey", QLatin1String("Bad value!")); FileSpecification inv2(json); QCOMPARE(inv2.format(), FileSpecification::InvalidFileSpecification); // filename, but no contents json.insert("filename", QLatin1String("Bad value!")); FileSpecification inv3(json); QCOMPARE(inv3.format(), FileSpecification::InvalidFileSpecification); FileSpecification inv4; QCOMPARE(inv4.format(), FileSpecification::InvalidFileSpecification); } void FileSpecificationTest::isValid() { FileSpecification pathSpec(QString("/some/path/to/a/file.ext")); QVERIFY(pathSpec.isValid()); FileSpecification contSpec(QString("file.ext"), QString("I'm input file text!\n")); QVERIFY(contSpec.isValid()); QJsonObject json; FileSpecification inv(json); QVERIFY(!inv.isValid()); } void FileSpecificationTest::toJsonObject() { FileSpecification pathSpec(QString("/some/path/to/a/file.ext")); QJsonObject pathJson = pathSpec.toJsonObject(); QCOMPARE(pathJson["path"].toString(), QString("/some/path/to/a/file.ext")); FileSpecification contSpec(QString("file.ext"), QString("I'm input file text!\n")); QJsonObject contJson = contSpec.toJsonObject(); QCOMPARE(contJson["filename"].toString(), QString("file.ext")); QCOMPARE(contJson["contents"].toString(), QString("I'm input file text!\n")); } void FileSpecificationTest::fileExists() { QTemporaryFile file; // filename isn't generated until open is called. file.open(); FileSpecification spec(&file, FileSpecification::PathFileSpecification); QVERIFY(spec.fileExists()); /// Always returns false for contents, since no path is known. spec = FileSpecification(&file, FileSpecification::ContentsFileSpecification); QVERIFY(!spec.fileExists()); file.close(); } void FileSpecificationTest::writeFile() { QTemporaryFile file; // filename isn't available until open is called. QVERIFY(file.open()); file.setTextModeEnabled(true); QString content("I'm sample input file contents!\n"); FileSpecification spec(file.fileName(), content); spec.writeFile(QFileInfo(file).dir()); QCOMPARE(QString(file.readAll()), content); file.close(); } void FileSpecificationTest::filename() { FileSpecification contSpec("file.ext", "contents\n"); QCOMPARE(contSpec.filename(), QString("file.ext")); FileSpecification pathSpec(QString("/path/to/some/file.ext")); QCOMPARE(pathSpec.filename(), QString("file.ext")); } void FileSpecificationTest::contents() { QTemporaryFile file; // filename isn't available until open is called. file.open(); QString content("I'm sample input file contents!\n"); FileSpecification spec(file.fileName(), content); QCOMPARE(spec.contents(), content); spec.writeFile(QFileInfo(file).dir()); QCOMPARE(spec.contents(), content); file.close(); } void FileSpecificationTest::filepath() { FileSpecification pathSpec(QString("/path/to/some/file.ext")); #ifdef _WIN32 QCOMPARE(pathSpec.filepath(), QString("C:/path/to/some/file.ext")); #else QCOMPARE(pathSpec.filepath(), QString("/path/to/some/file.ext")); #endif FileSpecification contSpec("file.ext", "contents\n"); QVERIFY(contSpec.filepath().isNull()); } void FileSpecificationTest::fileHasExtension() { FileSpecification pathSpec(QString("/path/to/some/file.ext")); QVERIFY(pathSpec.fileHasExtension()); pathSpec = FileSpecification(QString("/path/to/some/file")); QVERIFY(!pathSpec.fileHasExtension()); FileSpecification contSpec("file.ext", "contents\n"); QVERIFY(contSpec.fileHasExtension()); contSpec = FileSpecification("file", "contents\n"); QVERIFY(!contSpec.fileHasExtension()); } void FileSpecificationTest::fileBaseName() { FileSpecification pathSpec(QString("/path/to/some/file.ext")); QCOMPARE(pathSpec.fileBaseName(), QString("file")); pathSpec = FileSpecification(QString("/path/to/some/file")); QCOMPARE(pathSpec.fileBaseName(), QString("file")); FileSpecification contSpec("file.ext", "contents\n"); QCOMPARE(contSpec.fileBaseName(), QString("file")); contSpec = FileSpecification("file", "contents\n"); QCOMPARE(contSpec.fileBaseName(), QString("file")); } void FileSpecificationTest::fileExtension() { FileSpecification pathSpec(QString("/path/to/some/file.ext")); QCOMPARE(pathSpec.fileExtension(), QString("ext")); pathSpec = FileSpecification(QString("/path/to/some/file")); QVERIFY(pathSpec.fileExtension().isNull()); FileSpecification contSpec("file.ext", "contents\n"); QCOMPARE(contSpec.fileExtension(), QString("ext")); contSpec = FileSpecification("file", "contents\n"); QVERIFY(contSpec.fileExtension().isNull()); } QTEST_MAIN(FileSpecificationTest) #include "filespecificationtest.moc" molequeue-0.9.0/molequeue/app/testing/filestreamingdatatest.cpp000066400000000000000000000025051323436134600250550ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include #include "queues/uit/filestreamingdata.h" #include "referencestring.h" #include "xmlutils.h" class FileStreamingDataTest : public QObject { Q_OBJECT private slots: void testToXml(); }; void FileStreamingDataTest::testToXml() { MoleQueue::Uit::FileStreamingData fileData; fileData.setToken("TOKENDATA"); fileData.setFileName("/home/u/username/TESTFILE_UPLOAD"); fileData.setHostID(1); fileData.setUserName("username"); ReferenceString expected( "filestreamingdata-ref/filestreamingdata.xml"); QCOMPARE(fileData.toXml(), XmlUtils::stripWhitespace(expected)); } QTEST_MAIN(FileStreamingDataTest) #include "filestreamingdatatest.moc" molequeue-0.9.0/molequeue/app/testing/jobeventlisttest.cpp000066400000000000000000000062211323436134600241010ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include #include #include #include "queues/queueuit.h" #include "queues/uit/authenticateresponse.h" #include "queues/uit/jobeventlist.h" #include "referencestring.h" #include "xmlutils.h" class JobEventTest : public QObject { Q_OBJECT private slots: void initTestCase(); void testFromXml(); void testFromXmlWithJobId(); void testFromXmlWithJobIdsUser(); void testFromXmlWithJobIds(); private: QString m_jobEventXml; }; void JobEventTest::initTestCase() { m_jobEventXml = ReferenceString("jobeventlist-ref/jobeventlist.xml"); } void JobEventTest::testFromXml() { MoleQueue::Uit::JobEventList list = MoleQueue::Uit::JobEventList::fromXml(m_jobEventXml); QVERIFY(list.isValid()); QCOMPARE(list.jobEvents().size(), 6); } void JobEventTest::testFromXmlWithJobId() { QList jobIds; jobIds << 100535; MoleQueue::Uit::JobEventList list = MoleQueue::Uit::JobEventList::fromXml(m_jobEventXml, "username", jobIds); QVERIFY(list.isValid()); QCOMPARE(list.jobEvents().size(), 2); foreach(const MoleQueue::Uit::JobEvent &e, list.jobEvents()) { QCOMPARE(e.acctHost(), QString("ruby.erdc.hpc.mil")); QVERIFY(e.eventTime() == 1124393333); QCOMPARE(e.eventType(), QString("JOB_FINISH")); QCOMPARE(e.jobStatus(), QString("64")); QCOMPARE(e.jobQueue(), QString("biggiesmalls")); QVERIFY(e.jobId() == 100535); QCOMPARE(e.jobStatusText(), QString("done")); } } void JobEventTest::testFromXmlWithJobIdsUser() { QList jobIds; jobIds << 100535 << 100539; MoleQueue::Uit::JobEventList list = MoleQueue::Uit::JobEventList::fromXml(m_jobEventXml, "username2", jobIds); QVERIFY(list.isValid()); QCOMPARE(list.jobEvents().size(), 2); foreach(const MoleQueue::Uit::JobEvent &e, list.jobEvents()) { QCOMPARE(e.acctHost(), QString("ruby.erdc.hpc.mil")); QVERIFY(e.eventTime() == 1124393333); QCOMPARE(e.eventType(), QString("JOB_FINISH")); QCOMPARE(e.jobStatus(), QString("64")); QCOMPARE(e.jobQueue(), QString("biggiesmalls2")); QVERIFY(e.jobId() == 100539); QCOMPARE(e.jobStatusText(), QString("done")); } } void JobEventTest::testFromXmlWithJobIds() { QList jobIds; jobIds << 100535 << 100536; MoleQueue::Uit::JobEventList list = MoleQueue::Uit::JobEventList::fromXml(m_jobEventXml, "username", jobIds); QVERIFY(list.isValid()); QCOMPARE(list.jobEvents().size(), 3); } QTEST_MAIN(JobEventTest) #include "jobeventlisttest.moc" molequeue-0.9.0/molequeue/app/testing/jobmanagertest.cpp000066400000000000000000000050421323436134600234760ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "jobmanager.h" #include "job.h" #include using MoleQueue::Job; class JobManagerTest : public QObject { Q_OBJECT private: MoleQueue::JobManager m_jobManager; private slots: /// Called before the first test function is executed. void initTestCase(); /// Called after the last test function is executed. void cleanupTestCase(); /// Called before each test function is executed. void init(); /// Called after every test function. void cleanup(); // Set MoleQueue id to the current count()+1 void setNewJobIds(MoleQueue::Job); void testJobAboutToBeAdded(); void testLookupMoleQueueId(); }; void JobManagerTest::initTestCase() { connect(&m_jobManager, SIGNAL(jobAboutToBeAdded(MoleQueue::Job)), this, SLOT(setNewJobIds(MoleQueue::Job)), Qt::DirectConnection); } void JobManagerTest::cleanupTestCase() { } void JobManagerTest::init() { } void JobManagerTest::cleanup() { } void JobManagerTest::setNewJobIds(MoleQueue::Job job) { MoleQueue::IdType id = static_cast(m_jobManager.count()); job.setMoleQueueId(id); } void JobManagerTest::testJobAboutToBeAdded() { QSignalSpy spy(&m_jobManager, SIGNAL(jobAboutToBeAdded(MoleQueue::Job))); m_jobManager.newJob(); QCOMPARE(spy.count(), 1); m_jobManager.newJob(m_jobManager.jobAt(m_jobManager.count()-1).toJsonObject()); QCOMPARE(spy.count(), 2); } void JobManagerTest::testLookupMoleQueueId() { if (m_jobManager.count() != 2) { qDebug() << "Not enough jobs in the queuemanager. Skipping test."; return; } const MoleQueue::Job job1 = m_jobManager.jobAt(0); const MoleQueue::Job job2 = m_jobManager.jobAt(1); const MoleQueue::Job lookupJob1 = m_jobManager.lookupJobByMoleQueueId(1); const MoleQueue::Job lookupJob2 = m_jobManager.lookupJobByMoleQueueId(2); QCOMPARE(job1, lookupJob1); QCOMPARE(job2, lookupJob2); } QTEST_MAIN(JobManagerTest) #include "jobmanagertest.moc" molequeue-0.9.0/molequeue/app/testing/jobsubmissioninfotest.cpp000066400000000000000000000035031323436134600251330ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include #include "queues/uit/jobsubmissioninfo.h" #include "referencestring.h" #include "xmlutils.h" class JobSubmissionInfoTest : public QObject { Q_OBJECT private slots: void initTestCase(); void testFromXml(); void testJobNumberRegex(); private: QString m_jobSubmissionInfoXml; }; void JobSubmissionInfoTest::initTestCase() { m_jobSubmissionInfoXml = XmlUtils::stripWhitespace( ReferenceString("jobsubmissioninfo-ref/jobsubmissioninfo.xml")); } void JobSubmissionInfoTest::testFromXml() { MoleQueue::Uit::JobSubmissionInfo info = MoleQueue::Uit::JobSubmissionInfo::fromXml(m_jobSubmissionInfoXml); QVERIFY(info.isValid()); QVERIFY(info.jobNumber() == 343242); QCOMPARE(info.stdout(), QString("Job <75899> is submitted to debug queue.")); QCOMPARE(info.stderr(), QString("error")); } void JobSubmissionInfoTest::testJobNumberRegex() { QString testJobString = "234234.sdb\n"; QRegExp parser ("^(\\d+)\\.sdb$"); int index = parser.indexIn(testJobString.trimmed()); QVERIFY(index != -1); QCOMPARE(parser.cap(1), QString("234234")); } QTEST_MAIN(JobSubmissionInfoTest) #include "jobsubmissioninfotest.moc" molequeue-0.9.0/molequeue/app/testing/jsonrpctest.cpp000066400000000000000000000102521323436134600230460ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include "dummyconnection.h" #include "dummyconnectionlistener.h" #include "referencestring.h" #include #include #include #include #include class JsonRpcTest : public QObject { Q_OBJECT private: DummyConnection m_conn1; DummyConnection *m_conn2; DummyConnectionListener m_connList1; DummyConnectionListener *m_connList2; MoleQueue::JsonRpc m_jsonRpc; private slots: /// Called before the first test function is executed. void initTestCase(); /// Called after the last test function is executed. void cleanupTestCase(); /// Called before each test function is executed. void init(); /// Called after every test function. void cleanup(); void addConnectionListener(); void addConnection(); void messageReceived(); void removeConnection(); void removeConnectionListener(); void internalPing(); }; void JsonRpcTest::initTestCase() { m_conn2 = NULL; m_connList2 = NULL; } void JsonRpcTest::cleanupTestCase() { } void JsonRpcTest::init() { } void JsonRpcTest::cleanup() { } void JsonRpcTest::addConnectionListener() { QCOMPARE(m_jsonRpc.m_connections.size(), 0); m_jsonRpc.addConnectionListener(&m_connList1); QCOMPARE(m_jsonRpc.m_connections.size(), 1); m_connList2 = new DummyConnectionListener(this); m_jsonRpc.addConnectionListener(m_connList2); QCOMPARE(m_jsonRpc.m_connections.size(), 2); } void JsonRpcTest::addConnection() { QCOMPARE(m_jsonRpc.m_connections.size(), 2); QCOMPARE(m_jsonRpc.m_connections[&m_connList1].size(), 0); m_connList1.emitNewConnection(&m_conn1); QCOMPARE(m_jsonRpc.m_connections[&m_connList1].size(), 1); m_conn2 = new DummyConnection(this); m_connList1.emitNewConnection(m_conn2); QCOMPARE(m_jsonRpc.m_connections[&m_connList1].size(), 2); } void JsonRpcTest::messageReceived() { MoleQueue::Message dummyMsg(MoleQueue::Message::Request, &m_conn1); dummyMsg.setMethod("testMethod"); QSignalSpy spy(&m_jsonRpc, SIGNAL(messageReceived(MoleQueue::Message))); m_conn1.emitPacketReceived(dummyMsg); qApp->processEvents(QEventLoop::AllEvents, 1000); QCOMPARE(spy.count(), 1); } void JsonRpcTest::removeConnection() { // Destroying a connection should remove it from the JsonRpc instance. This // tests all code paths involved in removing a connection. QVERIFY(m_conn2 != NULL); QCOMPARE(m_jsonRpc.m_connections[&m_connList1].size(), 2); delete m_conn2; qApp->processEvents(QEventLoop::AllEvents, 1000); QCOMPARE(m_jsonRpc.m_connections[&m_connList1].size(), 1); } void JsonRpcTest::removeConnectionListener() { // Destroying a connectionlistener should remove it from the JsonRpc instance. // This tests all code paths involved in removing a connectionlistener. QVERIFY(m_connList2 != NULL); QCOMPARE(m_jsonRpc.m_connections.size(), 2); delete m_connList2; qApp->processEvents(QEventLoop::AllEvents, 1000); QCOMPARE(m_jsonRpc.m_connections.size(), 1); } void JsonRpcTest::internalPing() { ReferenceString request("jsonrpc-ref/internalPing-request.json"); ReferenceString response("jsonrpc-ref/internalPing-response.json"); DummyConnection connection; QJsonDocument doc(QJsonDocument::fromJson(request.toString().toLatin1())); m_jsonRpc.handleJsonValue(&connection, MoleQueue::EndpointIdType(), doc.object()); qApp->processEvents(); QCOMPARE(QString(connection.popMessage().toJson()), QString(response.toString().toLatin1())); } QTEST_MAIN(JsonRpcTest) #include "jsonrpctest.moc" molequeue-0.9.0/molequeue/app/testing/kerberoscredentialstest.cpp000066400000000000000000000022601323436134600254220ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include "queues/uit/kerberoscredentials.h" #include "referencestring.h" #include "xmlutils.h" class KerberosCredentialsTest : public QObject { Q_OBJECT private slots: void testToXml(); }; void KerberosCredentialsTest::testToXml() { ReferenceString expected("kerberoscredentials-ref/kerberoscredentials.xml"); MoleQueue::Uit::KerberosCredentials credentials("test", "test"); QCOMPARE(credentials.toXml(), XmlUtils::stripWhitespace(expected)); } QTEST_MAIN(KerberosCredentialsTest) #include "kerberoscredentialstest.moc" molequeue-0.9.0/molequeue/app/testing/messagetest.cpp000066400000000000000000000403001323436134600230110ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include #include "dummyconnection.h" #include "referencestring.h" #include "idtypeutils.h" #include #include using MoleQueue::Message; class MessageTest : public QObject { Q_OBJECT private: DummyConnection m_conn; private slots: /// Called before the first test function is executed. void initTestCase(); /// Called after the last test function is executed. void cleanupTestCase(); /// Called before each test function is executed. void init(); /// Called after every test function. void cleanup(); // Test simple set/get etc void sanityCheck(); // Test public methods void toJson(); // This indirectly (and more easily) tests toJsonObject(). void send(); void generateResponse(); void generateErrorResponse(); // Round trip reference messages void parse_data(); void parse(); // Check general message format error handling void parseErrorHandling(); // Test handling of message-type specific errors. Note that any message // identified as a notification or response cannot fail. // Calling parse() a malformed error message will not return false, but the // message will be replaced with a server error message (with the original // message in the error.data.origMessage member). void interpretRequest(); void interpretError(); }; void MessageTest::initTestCase() { } void MessageTest::cleanupTestCase() { } void MessageTest::init() { } void MessageTest::cleanup() { } void MessageTest::sanityCheck() { // type Message invalid; QCOMPARE(invalid.type(), Message::Invalid); Message request(Message::Request); QCOMPARE(request.type(), Message::Request); Message notification(Message::Notification); QCOMPARE(notification.type(), Message::Notification); Message response(Message::Response); QCOMPARE(response.type(), Message::Response); Message error(Message::Error); QCOMPARE(error.type(), Message::Error); // method request.setMethod("Test"); QCOMPARE(request.method(), QString("Test")); // params QJsonObject paramsObject; paramsObject.insert("test", QLatin1String("value")); request.setParams(paramsObject); QCOMPARE(request.params().toObject(), paramsObject); QJsonArray paramsArray; paramsArray.append(QString("Test")); request.setParams(paramsArray); QCOMPARE(request.params().toArray(), paramsArray); // result response.setResult(true); QCOMPARE(response.result().toBool(false), true); // errorCode int testInt = 867-5309; error.setErrorCode(testInt); QCOMPARE(error.errorCode(), testInt); // errorMessage QString testMessage = "Test Error Message"; error.setErrorMessage(testMessage); QCOMPARE(error.errorMessage(), testMessage); // errorData error.setErrorData(false); QCOMPARE(error.errorData().toBool(true), false); // id MoleQueue::MessageIdType id(QString("IDIDIDID")); error.setId(id); QCOMPARE(error.id(), id); // connection error.setConnection(&m_conn); QCOMPARE(error.connection(), &m_conn); // endpoint MoleQueue::EndpointIdType endpoint("I'm an endpoint!!"); error.setEndpoint(endpoint); QCOMPARE(error.endpoint(), endpoint); } void MessageTest::toJson() { // Misc objects used in testing: QJsonObject testObject; testObject.insert("test", QLatin1String("value")); QJsonArray testArray; testArray.append(QString("Test")); QJsonArray testCompositeArray; testCompositeArray.append(MoleQueue::idTypeToJson(MoleQueue::InvalidId)); testCompositeArray.append(testObject); testCompositeArray.append(testArray); testCompositeArray.append(true); testCompositeArray.append(5); testCompositeArray.append(5.36893473232); // This will be truncated to %.5f! testCompositeArray.append(QString("Abrakadabra")); // Test that the idtypeutils is working as expected. QVERIFY(testCompositeArray.first().isNull()); QCOMPARE(MoleQueue::toIdType(testCompositeArray.first()), MoleQueue::InvalidId); // Invalid message Message invalid; QCOMPARE(QString(invalid.toJson()), QString(ReferenceString("message-ref/invalidJson.json"))); // Request -- no params Message request(Message::Request); request.setMethod("testMethod"); request.setId(MoleQueue::MessageIdType(1)); QCOMPARE(QString(request.toJson()), QString(ReferenceString("message-ref/requestJson-noParams.json"))); // Request -- object params request.setParams(testObject); QCOMPARE(QString(request.toJson()), QString(ReferenceString("message-ref/requestJson-objectParams.json"))); // Request -- array params request.setParams(testArray); QCOMPARE(QString(request.toJson()), QString(ReferenceString("message-ref/requestJson-arrayParams.json"))); // Notification -- no params Message notification(Message::Notification); notification.setMethod("poke"); QCOMPARE(QString(notification.toJson()), QString(ReferenceString("message-ref/notificationJson-noParams.json"))); // Notification -- object params notification.setParams(testObject); QCOMPARE(QString(notification.toJson()), QString(ReferenceString("message-ref/notificationJson-objectParams.json"))); // Notification -- array params notification.setParams(testArray); QCOMPARE(QString(notification.toJson()), QString(ReferenceString("message-ref/notificationJson-arrayParams.json"))); // Response Message response(Message::Response); response.setId(MoleQueue::MessageIdType(42)); response.setMethod("Won't be in JSON string for response."); response.setResult(testCompositeArray); QCOMPARE(QString(response.toJson()), QString(ReferenceString("message-ref/responseJson.json"))); // Error -- no data Message error(Message::Error); error.setId(MoleQueue::MessageIdType(13)); error.setMethod("Won't be in JSON string for error."); error.setErrorCode(666); error.setErrorMessage("Server is possessed."); QCOMPARE(QString(error.toJson()), QString(ReferenceString("message-ref/errorJson-noData.json"))); // Error -- primitive data error.setErrorData(55); QCOMPARE(QString(error.toJson()), QString(ReferenceString("message-ref/errorJson-primData.json"))); // Error -- object data error.setErrorData(testObject); QCOMPARE(QString(error.toJson()), QString(ReferenceString("message-ref/errorJson-objectData.json"))); // Error -- array data error.setErrorData(testArray); QCOMPARE(QString(error.toJson()), QString(ReferenceString("message-ref/errorJson-arrayData.json"))); } void MessageTest::send() { QCOMPARE(m_conn.messageCount(), 0); // Invalid message, no connection set Message invalidMessage; QCOMPARE(invalidMessage.send(), false); QCOMPARE(m_conn.messageCount(), 0); // Invalid message, no connection set invalidMessage.setConnection(&m_conn); QCOMPARE(invalidMessage.send(), false); QCOMPARE(m_conn.messageCount(), 0); // Valid message, no connection set Message request(Message::Request); request.setMethod("testMethod"); QCOMPARE(request.send(), false); QCOMPARE(m_conn.messageCount(), 0); // Test id generation for requests request.setConnection(&m_conn); QVERIFY(request.id().isNull()); QCOMPARE(request.send(), true); QVERIFY(!request.id().isNull()); QCOMPARE(m_conn.messageCount(), 1); // Id should match the message received by the connection: Message connMessage = m_conn.popMessage(); MoleQueue::MessageIdType requestId = request.id(); QCOMPARE(requestId, connMessage.id()); // Resending the request should assign a different id. QCOMPARE(request.send(), true); QVERIFY(!request.id().isNull()); QCOMPARE(m_conn.messageCount(), 1); // The new id should not match the old one: connMessage = m_conn.popMessage(); QVERIFY(requestId != connMessage.id()); // Sending any other type of message should not modify the ids. MoleQueue::MessageIdType testId(QLatin1String("testId")); // Notifications // (no id testing -- ids are not used.) Message notification(Message::Notification, &m_conn); notification.setMethod("testMethod"); QCOMPARE(notification.send(), true); QCOMPARE(m_conn.messageCount(), 1); m_conn.popMessage(); // Response Message response(Message::Response, &m_conn); response.setId(testId); response.setMethod("testMethod"); QCOMPARE(response.send(), true); QCOMPARE(m_conn.messageCount(), 1); QCOMPARE(m_conn.popMessage().id(), testId); // Error Message error(Message::Error, &m_conn); error.setId(testId); error.setErrorCode(2); error.setErrorMessage("Test error"); QCOMPARE(error.send(), true); QCOMPARE(m_conn.messageCount(), 1); QCOMPARE(m_conn.popMessage().id(), testId); } void MessageTest::generateResponse() { Message request(Message::Request, &m_conn, MoleQueue::EndpointIdType("erg")); request.setMethod("testMethod"); request.setId(MoleQueue::MessageIdType(QLatin1String("testId"))); Message response = request.generateResponse(); QCOMPARE(response.type(), Message::Response); QCOMPARE(request.connection(), response.connection()); QCOMPARE(request.endpoint(), response.endpoint()); QCOMPARE(request.method(), response.method()); QCOMPARE(request.id(), response.id()); } void MessageTest::generateErrorResponse() { Message request(Message::Request, &m_conn, MoleQueue::EndpointIdType("erg")); request.setMethod("testMethod"); request.setId(MoleQueue::MessageIdType(QLatin1String("testId"))); Message error = request.generateErrorResponse(); QCOMPARE(error.type(), Message::Error); QCOMPARE(request.connection(), error.connection()); QCOMPARE(request.endpoint(), error.endpoint()); QCOMPARE(request.method(), error.method()); QCOMPARE(request.id(), error.id()); } void MessageTest::parse_data() { QTest::addColumn("filename"); // Test the parser by round tripping the reference sets. QTest::newRow("errorJson-arrayData.json") << "errorJson-arrayData.json"; QTest::newRow("errorJson-primData.json") << "errorJson-primData.json"; QTest::newRow("notificationJson-noParams.json") << "notificationJson-noParams.json"; QTest::newRow("requestJson-noParams.json") << "requestJson-noParams.json"; QTest::newRow("errorJson-noData.json") << "errorJson-noData.json"; QTest::newRow("notificationJson-objectParams.json") << "notificationJson-objectParams.json"; QTest::newRow("requestJson-objectParams.json") << "requestJson-objectParams.json"; QTest::newRow("errorJson-objectData.json") << "errorJson-objectData.json"; QTest::newRow("notificationJson-arrayParams.json") << "notificationJson-arrayParams.json"; QTest::newRow("requestJson-arrayParams.json") << "requestJson-arrayParams.json"; QTest::newRow("responseJson.json") << "responseJson.json"; } void MessageTest::parse() { // Fetch the current filename and load it into a ReferenceString QFETCH(QString, filename); filename.prepend("message-ref/"); ReferenceString refStr(filename); // Parse the doc and create a message QJsonDocument doc = QJsonDocument::fromJson(refStr.toString().toLatin1()); QVERIFY(doc.isObject()); QJsonObject refObj = doc.object(); QJsonValue origId; // Fixup the ids so that the MessageIdManager can resolve them. if (refObj.contains("id")) { Message request(Message::Request, &m_conn); request.setMethod("testMethod"); QVERIFY(request.send()); m_conn.popMessage(); origId = refObj.value("id"); refObj["id"] = request.id(); } // Parse the message Message message(refObj); QVERIFY(message.parse()); // Reset the id if needed if (!origId.isNull()) message.setId(origId); // Compare strings QCOMPARE(refStr.toString(), QString(message.toJson())); } void MessageTest::parseErrorHandling() { // If the message isn't raw, we should return true -- nothing to parse! QVERIFY(Message(Message::Request).parse()); QVERIFY(Message(Message::Notification).parse()); QVERIFY(Message(Message::Response).parse()); QVERIFY(Message(Message::Error).parse()); QVERIFY(Message(Message::Invalid).parse()); // Construct a valid object and verify that it parses. QJsonObject validObj; validObj.insert("jsonrpc", QLatin1String("2.0")); validObj.insert("id", QLatin1String("5")); validObj.insert("method", QLatin1String("testMethod")); QVERIFY(Message(validObj).parse()); // This will be our test object. QJsonObject obj; // Must contain 'jsonrpc' member obj = validObj; obj.remove("jsonrpc"); QVERIFY(!Message(obj).parse()); // 'jsonrpc' member must be a string obj = validObj; obj["jsonrpc"] = 2.0; QVERIFY(!Message(obj).parse()); // 'jsonrpc' member must be exactly "2.0" obj = validObj; obj["jsonrpc"] = QString("1.0 + 1.0"); QVERIFY(!Message(obj).parse()); // Must have either id or method obj = validObj; obj.remove("id"); obj.remove("method"); QVERIFY(!Message(obj).parse()); // If present, method must be a string obj = validObj; obj["method"] = true; QVERIFY(!Message(obj).parse()); } void MessageTest::interpretRequest() { // Construct a valid object and verify that it parses. QJsonObject validObj; validObj.insert("jsonrpc", QLatin1String("2.0")); validObj.insert("id", QLatin1String("5")); validObj.insert("method", QLatin1String("testMethod")); QVERIFY(Message(validObj).parse()); // This will be our test object. QJsonObject obj; // If params is present, it must be a structured type (i.e. array or object) obj = validObj; obj["params"] = true; QVERIFY(!Message(obj).parse()); } // Register the id, attempt to parse, and check that the parsed error object // shows a server error occurred (if serverErr is true). #define TEST_ERROR_PARSING(obj, serverErr) \ { \ if (obj.contains("id")) { \ Message dummyRequest(Message::Request, &m_conn); \ dummyRequest.setMethod("testMethod"); \ QVERIFY(dummyRequest.send()); \ m_conn.popMessage(); \ obj["id"] = dummyRequest.id(); \ } \ Message msg(obj); \ QVERIFY(msg.parse()); \ QCOMPARE(msg.type(), Message::Error); \ if (serverErr) \ QCOMPARE(msg.errorCode(), -32000); \ else \ QVERIFY(msg.errorCode() != -32000); \ } void MessageTest::interpretError() { // If the error is malformed, parsing will NOT fail, as we cannot send an // error reply. Instead, the error metadata is replaced with a server error // (code = -32000) // Construct a valid object and verify that it parses. QJsonObject validErrorObj; validErrorObj.insert("code", 2); validErrorObj.insert("message", QString("Error message")); validErrorObj.insert("data", 5); QJsonObject validObj; validObj.insert("jsonrpc", QLatin1String("2.0")); validObj.insert("id", QLatin1String("5")); validObj.insert("error", validErrorObj); TEST_ERROR_PARSING(validObj, false); // This will be our test object. QJsonObject obj; QJsonObject errorObj; // error must be an object obj = validObj; obj["error"] = 5; TEST_ERROR_PARSING(obj, true); // error.code must be present errorObj = validErrorObj; errorObj.remove("code"); obj = validObj; obj["error"] = errorObj; TEST_ERROR_PARSING(obj, true); // error.code must be numeric errorObj = validErrorObj; errorObj["code"] = true; obj = validObj; obj["error"] = errorObj; TEST_ERROR_PARSING(obj, true); // error.code must be integral errorObj = validErrorObj; errorObj["code"] = 2.3; obj = validObj; obj["error"] = errorObj; TEST_ERROR_PARSING(obj, true); // error.message must be present errorObj = validErrorObj; errorObj.remove("message"); obj = validObj; obj["error"] = errorObj; TEST_ERROR_PARSING(obj, true); // error.message must be a string errorObj = validErrorObj; errorObj["message"] = 2.66; obj = validObj; obj["error"] = errorObj; TEST_ERROR_PARSING(obj, true); } QTEST_MAIN(MessageTest) #include "messagetest.moc" molequeue-0.9.0/molequeue/app/testing/molequeuetestconfig.h.in000066400000000000000000000014271323436134600246350ustar00rootroot00000000000000/* Autogenerated header. Edit molequeueconfig.cmake.in for permanent changes. */ /* Location of test reference data. */ #cmakedefine MoleQueue_TESTDATA_DIR "@MoleQueue_TESTDATA_DIR@" /* Location of testing scripts. */ #cmakedefine MoleQueue_TESTSCRIPT_DIR "@MoleQueue_TESTSCRIPT_DIR@" /* Location of testing scripts. */ #cmakedefine MoleQueue_TESTEXEC_DIR "@MoleQueue_TESTEXEC_DIR@" /* Path to python 2.x executable, if available. */ #cmakedefine MoleQueue_PYTHON_EXECUTABLE "@MoleQueue_PYTHON_EXECUTABLE@" /* Whether the build has ZeroMQ enabled. */ #cmakedefine MoleQueue_HAS_ZMQ /* Location of the MoleQueue sources. */ #cmakedefine MoleQueue_SOURCE_DIR "@MoleQueue_SOURCE_DIR@" /* Location of the MoleQueue build dir. */ #cmakedefine MoleQueue_BINARY_DIR "@MoleQueue_BINARY_DIR@" molequeue-0.9.0/molequeue/app/testing/pbstest.cpp000066400000000000000000000114531323436134600221600ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include "queues/pbs.h" class QueuePbsTest : public QObject { Q_OBJECT private: MoleQueue::QueuePbs m_queue; private slots: /// Called before the first test function is executed. void initTestCase(); /// Called after the last test function is executed. void cleanupTestCase(); /// Called before each test function is executed. void init(); /// Called after every test function. void cleanup(); void sanityCheck(); void testParseJobId(); void testParseQueueLine(); }; void QueuePbsTest::initTestCase() { } void QueuePbsTest::cleanupTestCase() { } void QueuePbsTest::init() { } void QueuePbsTest::cleanup() { } void QueuePbsTest::sanityCheck() { QCOMPARE(m_queue.typeName(), QString("PBS/Torque")); QString testString = "some.host.somewhere"; m_queue.setHostName(testString); QCOMPARE(m_queue.hostName(), testString); testString = "aUser"; m_queue.setUserName(testString); QCOMPARE(m_queue.userName(), testString); m_queue.setSshPort(6887); QCOMPARE(m_queue.sshPort(), 6887); testString = "/some/path"; m_queue.setWorkingDirectoryBase(testString); QCOMPARE(m_queue.workingDirectoryBase(), testString); testString = "subComm"; m_queue.setSubmissionCommand(testString); QCOMPARE(m_queue.submissionCommand(), testString); testString = "reqComm"; m_queue.setRequestQueueCommand(testString); QCOMPARE(m_queue.requestQueueCommand(), testString); } void QueuePbsTest::testParseJobId() { QString submissionOutput = "1234.not.a.real.host"; MoleQueue::IdType jobId; QVERIFY(m_queue.parseQueueId(submissionOutput, &jobId)); QCOMPARE(jobId, static_cast(1234)); } void QueuePbsTest::testParseQueueLine() { QString line; MoleQueue::IdType jobId; MoleQueue::JobState state; // First some invalid lines line = "Job id Name User Time Use S Queue"; QVERIFY(!m_queue.parseQueueLine(line, &jobId, &state)); line = "---------------- ---------------- ---------------- -------- - -----"; QVERIFY(!m_queue.parseQueueLine(line, &jobId, &state)); // "I" for "I"nvalid status (doesn't exist in PBS) line = "4807.host scatter user01 12:56:34 I batch"; QVERIFY(!m_queue.parseQueueLine(line, &jobId, &state)); // Check various states: line = "231.host scatter user01 12:56:34 R batch"; QVERIFY(m_queue.parseQueueLine(line, &jobId, &state)); QCOMPARE(jobId, static_cast(231)); QCOMPARE(state, MoleQueue::RunningRemote); line = "232.host scatter user01 12:56:34 E batch"; QVERIFY(m_queue.parseQueueLine(line, &jobId, &state)); QCOMPARE(jobId, static_cast(232)); QCOMPARE(state, MoleQueue::RunningRemote); line = "233.host scatter user01 12:56:34 C batch"; QVERIFY(m_queue.parseQueueLine(line, &jobId, &state)); QCOMPARE(jobId, static_cast(233)); QCOMPARE(state, MoleQueue::RunningRemote); line = "234.host scatter user01 12:56:34 Q batch"; QVERIFY(m_queue.parseQueueLine(line, &jobId, &state)); QCOMPARE(jobId, static_cast(234)); QCOMPARE(state, MoleQueue::QueuedRemote); line = "235.host scatter user01 12:56:34 H batch"; QVERIFY(m_queue.parseQueueLine(line, &jobId, &state)); QCOMPARE(jobId, static_cast(235)); QCOMPARE(state, MoleQueue::QueuedRemote); line = "236.host scatter user01 12:56:34 T batch"; QVERIFY(m_queue.parseQueueLine(line, &jobId, &state)); QCOMPARE(jobId, static_cast(236)); QCOMPARE(state, MoleQueue::QueuedRemote); line = "237.host scatter user01 12:56:34 W batch"; QVERIFY(m_queue.parseQueueLine(line, &jobId, &state)); QCOMPARE(jobId, static_cast(237)); QCOMPARE(state, MoleQueue::QueuedRemote); line = "238.host scatter user01 12:56:34 S batch"; QVERIFY(m_queue.parseQueueLine(line, &jobId, &state)); QCOMPARE(jobId, static_cast(238)); QCOMPARE(state, MoleQueue::QueuedRemote); } QTEST_MAIN(QueuePbsTest) #include "pbstest.moc" molequeue-0.9.0/molequeue/app/testing/programtest.cpp000066400000000000000000000024671323436134600230500ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include "program.h" class ProgramTest : public QObject { Q_OBJECT private: private slots: /// Called before the first test function is executed. void initTestCase(); /// Called after the last test function is executed. void cleanupTestCase(); /// Called before each test function is executed. void init(); /// Called after every test function. void cleanup(); void runTest(); }; void ProgramTest::initTestCase() { } void ProgramTest::cleanupTestCase() { } void ProgramTest::init() { } void ProgramTest::cleanup() { } void ProgramTest::runTest() { qDebug() << "No tests implemented yet!"; } QTEST_MAIN(ProgramTest) #include "programtest.moc" molequeue-0.9.0/molequeue/app/testing/queuemanagertest.cpp000066400000000000000000000073021323436134600240510ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "queuemanager.h" #include "queue.h" #include #include #include class QueueDummy : public MoleQueue::Queue { Q_OBJECT public: QueueDummy(MoleQueue::QueueManager *parentManager) : MoleQueue::Queue ("Dummy", parentManager) { } public slots: bool submitJob(MoleQueue::Job) { return false; } void killJob(MoleQueue::Job) { } }; class QueueManagerTest : public QObject { Q_OBJECT private: MoleQueue::QueueManager m_queueManager; private slots: /// Called before the first test function is executed. void initTestCase(); /// Called after the last test function is executed. void cleanupTestCase(); /// Called before each test function is executed. void init(); /// Called after every test function. void cleanup(); void testAddQueue(); void testLookupQueue(); void testNumQueues(); void testToQueueList(); void testRemoveQueue(); void testCleanup(); }; void QueueManagerTest::initTestCase() { } void QueueManagerTest::cleanupTestCase() { } void QueueManagerTest::init() { } void QueueManagerTest::cleanup() { } void QueueManagerTest::testAddQueue() { QSignalSpy spy (&m_queueManager, SIGNAL(queueAdded(QString,MoleQueue::Queue*))); const QStringList & queues = m_queueManager.availableQueues(); QVERIFY(!queues.isEmpty()); QVERIFY(m_queueManager.addQueue("First Queue", queues.first()) != NULL); QVERIFY(m_queueManager.addQueue("Second Queue", queues.first()) != NULL); QVERIFY(m_queueManager.addQueue("Second Queue", queues.first()) == NULL); // duplicate name QCOMPARE(spy.count(), 2); } void QueueManagerTest::testLookupQueue() { QString queueName ("First Queue"); QCOMPARE(m_queueManager.lookupQueue(queueName)->name(), queueName); } void QueueManagerTest::testNumQueues() { QCOMPARE(m_queueManager.numQueues(), 2); } void QueueManagerTest::testToQueueList() { MoleQueue::QueueListType list = m_queueManager.toQueueList(); QStringList queueNames = list.keys(); qSort(queueNames); QCOMPARE(queueNames.size(), 2); QCOMPARE(queueNames[0], QString("First Queue")); QCOMPARE(queueNames[1], QString("Second Queue")); } void QueueManagerTest::testRemoveQueue() { QSignalSpy spy (&m_queueManager, SIGNAL(queueRemoved(QString,MoleQueue::Queue*))); QueueDummy notInManager (NULL); QCOMPARE(m_queueManager.removeQueue(¬InManager), false); QCOMPARE(m_queueManager.removeQueue("notInManager"), false); QCOMPARE(m_queueManager.removeQueue("First Queue"), true); QCOMPARE(m_queueManager.numQueues(), 1); QCOMPARE(m_queueManager.removeQueue(m_queueManager.queues().first()), true); QCOMPARE(m_queueManager.numQueues(), 0); QCOMPARE(spy.count(), 2); } void QueueManagerTest::testCleanup() { MoleQueue::QueueManager *manager = new MoleQueue::QueueManager (); const QStringList & queues = manager->availableQueues(); QVERIFY(!queues.isEmpty()); QPointer q = manager->addQueue("", queues.first()); delete manager; manager = NULL; QCOMPARE(q.data(), static_cast(NULL)); } QTEST_MAIN(QueueManagerTest) #include "queuemanagertest.moc" molequeue-0.9.0/molequeue/app/testing/queueremotetest.cpp000066400000000000000000000377441323436134600237470ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include "dummyqueueremote.h" #include "dummyqueuemanager.h" #include "dummyserver.h" #include "filesystemtools.h" #include "job.h" #include "jobmanager.h" #include "program.h" #include #include using namespace MoleQueue; class QueueRemoteTest : public QObject { Q_OBJECT private: DummyServer m_server; DummyQueueRemote *m_queue; private slots: /// Called before the first test function is executed. void initTestCase(); /// Called after the last test function is executed. void cleanupTestCase(); /// Called before each test function is executed. void init(); /// Called after every test function. void cleanup(); void sanityCheck(); void testSubmitJob(); void testKillJob(); void testSubmissionPipeline(); void testFinalizePipeline(); void testKillPipeline(); void testQueueUpdate(); void testReplaceKeywords(); }; void QueueRemoteTest::initTestCase() { m_queue = qobject_cast (m_server.queueManager()->addQueue("Dummy", "Dummy", true)); m_queue->setWorkingDirectoryBase("/fake/remote/path"); Program *program = new Program(m_queue); program->setName("DummyProgram"); program->setExecutable(""); program->setArguments(""); program->setOutputFilename("output.out"); program->setLaunchSyntax(Program::REDIRECT); m_queue->addProgram(program); } void QueueRemoteTest::cleanupTestCase() { FileSystemTools::recursiveRemoveDirectory(m_server.workingDirectoryBase()); } void QueueRemoteTest::init() { } void QueueRemoteTest::cleanup() { } void QueueRemoteTest::sanityCheck() { QCOMPARE(m_queue->typeName(), QString("Dummy")); QString testString = "some.host.somewhere"; m_queue->setHostName(testString); QCOMPARE(m_queue->hostName(), testString); testString = "aUser"; m_queue->setUserName(testString); QCOMPARE(m_queue->userName(), testString); m_queue->setSshPort(6887); QCOMPARE(m_queue->sshPort(), 6887); testString = "/some/path"; m_queue->setWorkingDirectoryBase(testString); QCOMPARE(m_queue->workingDirectoryBase(), testString); testString = "subComm"; m_queue->setSubmissionCommand(testString); QCOMPARE(m_queue->submissionCommand(), testString); testString = "reqComm"; m_queue->setRequestQueueCommand(testString); QCOMPARE(m_queue->requestQueueCommand(), testString); testString = "killComm"; m_queue->setKillCommand(testString); QCOMPARE(m_queue->killCommand(), testString); } void QueueRemoteTest::testSubmitJob() { // valid job Job job = m_server.jobManager()->newJob(); job.setInputFile(FileSpecification("input.in", "\n")); QVERIFY(m_queue->submitJob(job)); // invalid job QVERIFY(!m_queue->submitJob(Job())); // verify queue state QCOMPARE(m_queue->m_pendingSubmission.size(), 1); QCOMPARE(m_queue->m_pendingSubmission.first(), job.moleQueueId()); } void QueueRemoteTest::testKillJob() { // Invalid job m_queue->killJob(Job()); // unknown job Job job = m_server.jobManager()->newJob(); m_queue->killJob(job); QCOMPARE(job.jobState(), Canceled); // pending job (from testSubmitJob) job = Job(m_server.jobManager(), m_queue->m_pendingSubmission.first()); m_queue->killJob(job); QCOMPARE(job.jobState(), Canceled); QCOMPARE(m_queue->m_pendingSubmission.size(), 0); // "Running" job: job = m_server.jobManager()->newJob(); job.setQueue("Dummy"); job.setQueueId(999); m_queue->m_jobs.insert(job.queueId(), job.moleQueueId()); m_queue->killJob(job); // Won't actually kill job, but starts the killPipeline QCOMPARE(m_queue->m_jobs.size(), 0); } void QueueRemoteTest::testSubmissionPipeline() { /////////////////////// // submitPendingJobs // /////////////////////// // empty pending queue QCOMPARE(m_queue->m_pendingSubmission.size(), 0); m_queue->submitPendingJobs(); QCOMPARE(m_queue->m_pendingSubmission.size(), 0); // Create and submit "actual fake" job: Job job = m_server.jobManager()->newJob(); job.setQueue("Dummy"); job.setProgram("DummyProgram"); job.setDescription("DummyJob"); job.setInputFile(FileSpecification("input.in", "do stuff, return answers.")); job.setOutputDirectory(job.localWorkingDirectory() + "/../output"); job.setCleanRemoteFiles(true); job.setCleanLocalWorkingDirectory(true); m_queue->submitJob(job); QCOMPARE(m_queue->m_pendingSubmission.size(), 1); m_queue->submitPendingJobs(); // calls beginJobSubmission QCOMPARE(m_queue->m_pendingSubmission.size(), 0); //////////////////////// // beginJobSubmission // (calls writeInputFiles, copyInputFilesToHost) //////////////////////// ///////////////////// // writeInputFiles // ///////////////////// // Check that input files were written: Program *program = m_queue->lookupProgram(job.program()); QVERIFY(program != NULL); QString inputFileName = job.localWorkingDirectory() + "/" + job.inputFile().filename(); QVERIFY(QFile::exists(inputFileName)); QFile inputFile(inputFileName); QVERIFY(inputFile.open(QFile::ReadOnly | QFile::Text)); QCOMPARE(QString(inputFile.readAll()), job.inputFile().contents()); QString launchScriptFileName = job.localWorkingDirectory() + "/" + m_queue->launchScriptName(); QVERIFY(QFile::exists(launchScriptFileName)); QFile launchScriptFile(launchScriptFileName); QVERIFY(launchScriptFile.open(QFile::ReadOnly | QFile::Text)); QCOMPARE(QString(launchScriptFile.readAll()), QString("Run job 4!!\n")); ////////////////////////// // copyInputFilesToHost // ////////////////////////// // validate the ssh command DummySshCommand *ssh = m_queue->getDummySshCommand(); QCOMPARE(ssh->getDummyCommand(), QString("scp")); // Verify the local path manually with a regex, since the absolute path is // platform dependent QStringList dummyArgs = ssh->getDummyArgs(); QVERIFY(QRegExp("^.+MoleQueue-dummyServer/+jobs/+4$").exactMatch( dummyArgs.at(6))); dummyArgs.removeAt(6); QCOMPARE(dummyArgs, QStringList() << "-q" << "-S" << "ssh" << "-P" << "6887" << "-r" << "aUser@some.host.somewhere:/some/path/4" ); QCOMPARE(ssh->data().value(), job); // Fake the process output. Pretend that the remote working dir hasn't been // created yet. ssh->setDummyExitCode(1); ssh->setDummyOutput("No such file or directory"); ssh->emitDummyRequestComplete(); // triggers inputFilesCopied ////////////////////// // inputFilesCopied // // Should detect that the parent dir doesn't exist ////////////////////// // and call create remote directory /////////////////////////// // createRemoteDirectory // /////////////////////////// // Grab the dummy ssh command from the queue and validate its contents ssh = m_queue->getDummySshCommand(); QCOMPARE(ssh->getDummyCommand(), QString("ssh")); QCOMPARE(ssh->getDummyArgs(), QStringList() << "-q" << "-p" << "6887" << "aUser@some.host.somewhere" << "mkdir -p /some/path" ); QCOMPARE(ssh->data().value(), job); // Fake the process output ssh->setDummyExitCode(0); ssh->emitDummyRequestComplete(); // triggers remoteDirectoryCreated //////////////////////////// // remoteDirectoryCreated // // calls copyInputFilesToHost //////////////////////////// ////////////////////////// // copyInputFilesToHost // ////////////////////////// // validate the ssh command ssh = m_queue->getDummySshCommand(); QCOMPARE(ssh->getDummyCommand(), QString("scp")); // Verify the local path manually with a regex, since the absolute path is // platform dependent dummyArgs = ssh->getDummyArgs(); QVERIFY(QRegExp("^.+MoleQueue-dummyServer/+jobs/+4$").exactMatch( dummyArgs.at(6))); dummyArgs.removeAt(6); QCOMPARE(dummyArgs, QStringList() << "-q" << "-S" << "ssh" << "-P" << "6887" << "-r" << "aUser@some.host.somewhere:/some/path/4" ); QCOMPARE(ssh->data().value(), job); // Fake the process output ssh->setDummyExitCode(0); ssh->emitDummyRequestComplete(); // triggers inputFilesCopied ////////////////////// // inputFilesCopied // // calls submitJobToRemoteQueue ////////////////////// //////////////////////////// // submitJobToRemoteQueue // //////////////////////////// // validate the ssh command ssh = m_queue->getDummySshCommand(); QCOMPARE(ssh->getDummyCommand(), QString("ssh")); QCOMPARE(ssh->getDummyArgs(), QStringList() << "-q" << "-p" << "6887" << "aUser@some.host.somewhere" << "cd /some/path/4 && subComm launcher.dummy" ); QCOMPARE(ssh->data().value(), job); // Fake the process output ssh->setDummyExitCode(0); ssh->emitDummyRequestComplete(); // triggers jobSubmittedToRemoteQueue /////////////////////////////// // jobSubmittedToRemoteQueue // /////////////////////////////// // Check that the job has been updated QCOMPARE(m_queue->m_jobs.keys().size(), 1); QCOMPARE(job.queueId(), static_cast(12)); QCOMPARE(job.jobState(), Submitted); } void QueueRemoteTest::testFinalizePipeline() { // Kick off the finalize pipeline: QCOMPARE(m_queue->m_jobs.keys().size(), 1); Job job = m_server.jobManager()->lookupJobByMoleQueueId( m_queue->m_jobs.values().first()); m_queue->beginFinalizeJob(m_queue->m_jobs.keys().first()); ////////////////////// // beginFinalizeJob // (calls finalizeJobCopyFromServer) ////////////////////// QCOMPARE(m_queue->m_jobs.size(), 0); /////////////////////////////// // finalizeJobCopyFromServer // /////////////////////////////// // validate the ssh command DummySshCommand *ssh = m_queue->getDummySshCommand(); QCOMPARE(ssh->getDummyCommand(), QString("scp")); // Verify the local path manually with a regex, since the absolute path is // platform dependent QStringList dummyArgs = ssh->getDummyArgs(); QVERIFY(QRegExp("^.+MoleQueue-dummyServer/+jobs/+4/+\\.\\.$").exactMatch( dummyArgs.at(7))); dummyArgs.removeAt(7); QCOMPARE(dummyArgs, QStringList() << "-q" << "-S" << "ssh" << "-P" << "6887" << "-r" << "aUser@some.host.somewhere:/some/path/4" ); QCOMPARE(ssh->data().value(), job); // Fake the process output ssh->setDummyExitCode(0); ssh->emitDummyRequestComplete(); // triggers finalizeJobOutputCopiedFromServer /////////////////////////////////////// // finalizeJobOutputCopiedFromServer // (calls /////////////////////////////////////// finalizeJobCopyToCustomDestination) //////////////////////////////////////// // finalizeJobCopyToCustomDestination // (calls recursiveCopyDirectory //////////////////////////////////////// and finalizeJobCleanup) //////////////////////////// // recursiveCopyDirectory // //////////////////////////// QCOMPARE(QDir(job.outputDirectory()).entryList(), QStringList() << "." << ".." << "input.in" << "launcher.dummy" << "mqjobinfo.json"); //////////////////////// // finalizeJobCleanup // (calls cleanLocalDirectory //////////////////////// and cleanRemoteDirectory) QCOMPARE(job.jobState(), Finished); ///////////////////////// // cleanLocalDirectory // ///////////////////////// QCOMPARE(QDir(job.localWorkingDirectory()).entryList(), QStringList() << "." << ".."); ////////////////////////// // cleanRemoteDirectory // ////////////////////////// // validate the ssh command ssh = m_queue->getDummySshCommand(); QCOMPARE(ssh->getDummyCommand(), QString("ssh")); QCOMPARE(ssh->getDummyArgs(), QStringList() << "-q" << "-p" << "6887" << "aUser@some.host.somewhere" << "rm -rf /some/path/4" ); QCOMPARE(ssh->data().value(), job); // Fake the process output ssh->setDummyExitCode(0); ssh->emitDummyRequestComplete(); // triggers remoteDirectoryCleaned //////////////////////////// // remoteDirectoryCleaned // //////////////////////////// // no state changes to check } void QueueRemoteTest::testKillPipeline() { // Fake a submitted job Job job = m_server.jobManager()->newJob(); job.setQueue("Dummy"); job.setQueueId(988); m_queue->m_jobs.insert(job.queueId(), job.moleQueueId()); // kill the job m_queue->killJob(job); // calls beginKillJob ////////////////// // beginKillJob // ////////////////// // validate the ssh command DummySshCommand *ssh = m_queue->getDummySshCommand(); QCOMPARE(ssh->getDummyCommand(), QString("ssh")); QCOMPARE(ssh->getDummyArgs(), QStringList() << "-q" << "-p" << "6887" << "aUser@some.host.somewhere" << "killComm 988" ); QCOMPARE(ssh->data().value(), job); // Fake the process output ssh->setDummyExitCode(0); ssh->emitDummyRequestComplete(); // triggers endKillJob //////////////// // endKillJob // //////////////// QCOMPARE(job.jobState(), Canceled); } void QueueRemoteTest::testQueueUpdate() { QCOMPARE(m_queue->m_jobs.size(), 0); QString output; QList jobs; for (int state = static_cast(None); state < static_cast(Error); ++state) { // Add jobs with queueids Job job = m_server.jobManager()->newJob(); job.setQueue("Dummy"); job.setQueueId(static_cast(state)); jobs.append(job); // add to queue m_queue->m_jobs.insert(job.queueId(), job.moleQueueId()); // Create line of fake queue output output += QString("%1 %2\n").arg(idTypeToString(job.queueId())) .arg(jobStateToString(static_cast(state))); } m_queue->requestQueueUpdate(); //////////////////////// // requestQueueUpdate // //////////////////////// // validate the ssh command DummySshCommand *ssh = m_queue->getDummySshCommand(); QCOMPARE(ssh->getDummyCommand(), QString("ssh")); QCOMPARE(ssh->getDummyArgs(), QStringList() << "-q" << "-p" << "6887" << "aUser@some.host.somewhere" << "reqComm 0 1 2 3 4 5 6 7 8 " ); // Fake the process output ssh->setDummyExitCode(0); ssh->setDummyOutput(output); ssh->emitDummyRequestComplete(); // triggers handleQueueUpdate /////////////////////// // handleQueueUpdate // /////////////////////// foreach (const Job &job, jobs) { QCOMPARE(job.jobState(), static_cast(job.queueId())); } } void QueueRemoteTest::testReplaceKeywords() { // $$maxWallTime$$ QStringList list; m_queue->setDefaultMaxWallTime(1440); list << "$$maxWallTime$$ at start" << "At end $$maxWallTime$$" << "In middle $$maxWallTime$$ of line"; QString script = list.join("\n"); Job job = m_server.jobManager()->newJob(); job.setMaxWallTime(-1); m_queue->replaceKeywords(script, job); QCOMPARE(script, QString("24:00:00 at start\nAt end 24:00:00\n" "In middle 24:00:00 of line\n")); // $$$maxWallTime$$$ list.clear(); list << "Test first line" << "$$$maxWallTime$$$ at start" << "Test third line" << "At end $$$maxWallTime$$$" << "Test fifth line" << "In middle $$$maxWallTime$$$ of line" << "Test sixth line" << "Safe maxWallTime=$$maxWallTime$$"; script = list.join("\n"); m_queue->replaceKeywords(script, job); QCOMPARE(script, QString("Test first line\nTest third line\nTest fifth line\n" "Test sixth line\nSafe maxWallTime=24:00:00\n")); } QTEST_MAIN(QueueRemoteTest) #include "queueremotetest.moc" molequeue-0.9.0/molequeue/app/testing/queuetest.cpp000066400000000000000000000112051323436134600225130ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include "job.h" #include "jobmanager.h" #include "program.h" #include "queue.h" class DummyQueue : public MoleQueue::Queue { Q_OBJECT public: DummyQueue(const QString &queueName = "Dummy") : MoleQueue::Queue(queueName, NULL) {}; public slots: bool submitJob(MoleQueue::Job) { return false; } void killJob(MoleQueue::Job) { } }; class QueueTest : public QObject { Q_OBJECT private: DummyQueue m_queue; private slots: /// Called before the first test function is executed. void initTestCase(); /// Called after the last test function is executed. void cleanupTestCase(); /// Called before each test function is executed. void init(); /// Called after every test function. void cleanup(); void testNames(); void testAddProgram(); void testLookupProgram(); void testNumPrograms(); void testProgramNames(); void testRemoveProgram(); void testCleanup(); void testReplaceKeywords(); }; void QueueTest::initTestCase() { } void QueueTest::cleanupTestCase() { } void QueueTest::init() { } void QueueTest::cleanup() { } void QueueTest::testNames() { QCOMPARE(m_queue.name(), QString ("Dummy")); m_queue.setName("SomeQueue"); QCOMPARE(m_queue.name(), QString ("SomeQueue")); } void QueueTest::testAddProgram() { QSignalSpy spy (&m_queue, SIGNAL(programAdded(QString,MoleQueue::Program*))); MoleQueue::Program *p1 = new MoleQueue::Program(&m_queue); p1->setName("First Program"); MoleQueue::Program *p2 = new MoleQueue::Program(NULL); p2->setName("Second Program"); MoleQueue::Program *p2a = new MoleQueue::Program(&m_queue); p2a->setName("Second Program"); QVERIFY(m_queue.addProgram(p1)); QVERIFY(m_queue.addProgram(p2)); QVERIFY(!m_queue.addProgram(p2a)); // Duplicate name QCOMPARE(spy.count(), 2); } void QueueTest::testLookupProgram() { QString programName ("First Program"); QCOMPARE(m_queue.lookupProgram(programName)->name(), programName); } void QueueTest::testNumPrograms() { QCOMPARE(m_queue.numPrograms(), 2); } void QueueTest::testProgramNames() { QStringList programNames = m_queue.programNames(); qSort(programNames); QCOMPARE(programNames.size(), 2); QCOMPARE(programNames[0], QString("First Program")); QCOMPARE(programNames[1], QString("Second Program")); } void QueueTest::testRemoveProgram() { QSignalSpy spy (&m_queue, SIGNAL(programRemoved(QString,MoleQueue::Program*))); MoleQueue::Program notInQueue; QCOMPARE(m_queue.removeProgram(¬InQueue ), false); QCOMPARE(m_queue.removeProgram("notInQueue"), false); QCOMPARE(m_queue.removeProgram("First Program"), true); QCOMPARE(m_queue.numPrograms(), 1); QCOMPARE(m_queue.removeProgram(m_queue.programs().first()), true); QCOMPARE(m_queue.numPrograms(), 0); QCOMPARE(spy.count(), 2); } void QueueTest::testCleanup() { DummyQueue *queue = new DummyQueue(); QPointer program = new MoleQueue::Program (); queue->addProgram(program.data()); delete queue; queue = NULL; QCOMPARE(program.data(), static_cast(NULL)); } void QueueTest::testReplaceKeywords() { QString script = "$$moleQueueId$$\n"; MoleQueue::JobManager jobManager; MoleQueue::Job job = jobManager.newJob(); DummyQueue queue; queue.replaceKeywords(script, job); QCOMPARE(script, QString("%1\n") .arg(MoleQueue::idTypeToString(job.moleQueueId()))); script = "$$numberOfCores$$\n"; job.setNumberOfCores(32); queue.replaceKeywords(script, job); QCOMPARE(script, QString("%1\n").arg(job.numberOfCores())); script = "Ain't no newline!"; queue.replaceKeywords(script, job); QCOMPARE(script, QString("Ain't no newline!\n")); // Job specific keywords: job.setKeywordReplacement("Failure", "Success"); script = "$$Failure$$\n"; queue.replaceKeywords(script, job); QCOMPARE(script, QString("Success\n")); // Unhandled keywords: script = "$$aTerriblyLongKeywordThatYouWontRememberAnddWilLieklyMissspel$$\n"; queue.replaceKeywords(script, job); QCOMPARE(script, QString("\n")); } QTEST_MAIN(QueueTest) #include "queuetest.moc" molequeue-0.9.0/molequeue/app/testing/referencestring.cpp000066400000000000000000000023131323436134600236540ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "referencestring.h" #include "molequeuetestconfig.h" #include #include ReferenceString::ReferenceString(const QString &filename) { QString realFilename = MoleQueue_TESTDATA_DIR + filename; QFile refFile(realFilename); if (!refFile.open(QFile::ReadOnly | QIODevice::Text)) { qDebug() << "Cannot access reference file" << realFilename; return; } m_refString = refFile.readAll(); refFile.close(); } QString ReferenceString::toString() const { return m_refString; } ReferenceString::operator QString&() { return m_refString; } molequeue-0.9.0/molequeue/app/testing/referencestring.h000066400000000000000000000016151323436134600233250ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef REFERENCESTRING_H_ #define REFERENCESTRING_H_ #include class ReferenceString { public: ReferenceString(const QString &filename); QString toString() const; operator QString&(); private: QString m_refString; }; #endif /* REFERENCESTRING_H_ */ molequeue-0.9.0/molequeue/app/testing/scripts/000077500000000000000000000000001323436134600214535ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/testing/scripts/sendRpcKill.py000066400000000000000000000015221323436134600242370ustar00rootroot00000000000000import molequeue as mq from getopt import getopt import sys import time debug = False socketname = "" def run_test(): global socketname if debug: print "Connecting to socket: %s"%socketname client = mq.Client() client.connect_to_server(socketname) res = client._send_rpc_kill_request(5) client.disconnect() if res == None: raise Exception("Connection timed out!") if res != True: return 1 return 0 def main(argv): # -s for socketname opts, args = getopt(argv, "s:") for opt, arg in opts: if opt == "-s": global socketname socketname = arg try: test_result = run_test() except Exception as ex: print ex test_result = 1 if debug: print "Exiting with code: %d"%test_result sys.stdout.flush() return test_result if __name__ == "__main__": sys.exit(main(sys.argv[1:])) molequeue-0.9.0/molequeue/app/testing/scripts/submitJob.py000066400000000000000000000051331323436134600237650ustar00rootroot00000000000000import molequeue as mq from getopt import getopt import sys import time from threading import Lock debug = False num_jobs = 1 socketname = "" clientId = 0 molequeue_ids_done = [] mq_id_lock = Lock() def run_test(): global socketname, molequeue_ids_done, clientId, mq_id_lock, num_jobs if debug: print "Client %s connecting to socket: %s"%(clientId, socketname) client = mq.Client() client.connect_to_server(socketname) def notification_callback(msg): global molequeue_ids_done, mq_id_lock try: if msg['method'] == 'jobStateChanged': if msg['params']['newState'] == 'Finished': moleQueueId = msg['params']['moleQueueId'] with mq_id_lock: molequeue_ids_done.append(moleQueueId) if debug: print "Job %d finished! (Client %s)"%(moleQueueId, clientId) except Exception as ex: print "Unexpected notification:", msg, ex sys.stdout.flush() client.register_notification_callback(notification_callback) molequeue_ids = [] for i in range(num_jobs): job = mq.Job() job.queue = "TestQueue" job.program = "TestProgram" job.description = "Test job %d from python client %s"%(i+1, clientId) job.popup_on_state_change = False molequeue_id = client.submit_job(job, 30) molequeue_ids.append(molequeue_id) if molequeue_id == None: # Timeout client.disconnect() raise Exception("Connection timed out!") if debug: print "Submitted job %d (Client %s)"%(molequeue_id, clientId) sys.stdout.flush() timeout = 30 mq_id_lock.acquire() while len(molequeue_ids) != len(molequeue_ids_done) and timeout > 0: if debug: print "Client %s waiting to finish (timeout=%d unmatchedIDs=%d)"%\ (clientId, timeout, len(molequeue_ids) - len(molequeue_ids_done)) sys.stdout.flush() timeout -= 1 mq_id_lock.release() time.sleep(1) mq_id_lock.acquire() mq_id_lock.release() client.disconnect() if timeout > 0: return 0 return 1 def main(argv): # -s for socketname # -c for clientId # -n for the number of jobs opts, args = getopt(argv, "s:c:n:") for opt, arg in opts: if opt == "-s": global socketname socketname = arg if opt == "-c": global clientId clientId = arg if opt == "-n": global num_jobs num_jobs = int(arg) try: test_result = run_test() except Exception as ex: print ex test_result = 1 if debug: print "Exiting with code: %d (client %s)"%(test_result, clientId) sys.stdout.flush() return test_result if __name__ == "__main__": sys.exit(main(sys.argv[1:])) molequeue-0.9.0/molequeue/app/testing/servertest.cpp000066400000000000000000000265361323436134600227120ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include "server.h" #include "molequeuetestconfig.h" #include "actionfactorymanager.h" #include "jobactionfactories/openwithactionfactory.h" #include "jobmanager.h" #include "program.h" #include #include "testing/dummyconnection.h" #include "testing/referencestring.h" #include "testing/testserver.h" #include "queue.h" #include "queuemanager.h" #include #include #include #include #include #include using namespace MoleQueue; class ServerTest : public QObject { Q_OBJECT private: QString m_connectionString; QLocalSocket m_testSocket; Server *m_server; LocalSocketConnectionListener * localSocketConnectionListener(); private slots: /// Called before the first test function is executed. void initTestCase(); /// Called after the last test function is executed. void cleanupTestCase(); /// Called before each test function is executed. void init(); /// Called after every test function. void cleanup(); void testStart(); void testStop(); void testForceStart(); void testNewConnection(); void testClientDisconnected(); void handleMessage_data(); void handleMessage(); void verifyOpenWithHandler(); }; void ServerTest::initTestCase() { // Change qsettings so that we don't overwrite the installed configuration: QString workDir = MoleQueue_BINARY_DIR "/Testing/Temporary/ServerTest"; QDir dir; dir.mkpath(workDir); QSettings::setPath(QSettings::NativeFormat, QSettings::UserScope, workDir + "/config"); QSettings settings; settings.setValue("workingDirectoryBase", workDir); m_connectionString = TestServer::getRandomSocketName(); m_server = new Server(this, m_connectionString); // Setup some fake queues/programs for rpc testing Queue *testQueue = m_server->queueManager()->addQueue("testQueue", "Local"); Program *testProgram = new Program(testQueue); testProgram->setName("testProgram"); testQueue->addProgram(testProgram); Queue *fakeQueue = m_server->queueManager()->addQueue("fakeQueue", "Local"); Program *fakeProgram1 = new Program(fakeQueue); fakeProgram1->setName("fakeProgram1"); fakeQueue->addProgram(fakeProgram1); Program *fakeProgram2 = new Program(fakeQueue); fakeProgram2->setName("fakeProgram2"); fakeQueue->addProgram(fakeProgram2); } void ServerTest::cleanupTestCase() { } void ServerTest::init() { } void ServerTest::cleanup() { } MoleQueue::LocalSocketConnectionListener * ServerTest::localSocketConnectionListener() { MoleQueue::LocalSocketConnectionListener *localListener = NULL; foreach(MoleQueue::ConnectionListener *listener, m_server->m_connectionListeners) { localListener = static_cast(listener); if (localListener) break; } assert(localListener != NULL); return localListener; } void ServerTest::testStart() { m_server->start(); //QCOMPARE(m_server.m_connectionListener->connectionString(), // QString("MoleQueue-testing")); } void ServerTest::testStop() { m_server->stop(true); //QVERIFY(m_server.m_connectionListener == NULL); } void ServerTest::testForceStart() { // For now exclude this on Windows named pipes do now throw an error // when you create one using the same name ... #ifndef _WIN32 // Start a duplicate server to take the socket address MoleQueue::Server dupServer(this, m_connectionString); dupServer.start(); // Attempt to start the server. Check that the AddressInUseError is emitted. QSignalSpy spy (m_server, SIGNAL(connectionError(MoleQueue::ConnectionListener::Error, const QString&))); m_server->start(); qApp->processEvents(QEventLoop::AllEvents, 1000); QCOMPARE(spy.count(), 1); QCOMPARE(spy.first().size(), 2); MoleQueue::ConnectionListener::Error err = spy.first()[0].value(); QString errString = spy.first()[1].toString(); QCOMPARE(err, MoleQueue::ConnectionListener::AddressInUseError); QCOMPARE(errString, QString("QLocalServer::listen: Address in use")); spy.clear(); // Force start server m_server->forceStart(); QVERIFY(spy.isEmpty()); // Check that m_server is now listening. QVERIFY(localSocketConnectionListener()->m_server->isListening()); dupServer.stop(); #endif } void ServerTest::testNewConnection() { // Restart server to reset state m_server->stop(); m_server->start(); int origConns = m_server->m_connections.size(); m_testSocket.connectToServer(m_connectionString); // Wait 5 seconds for a timeout. QTimer timer; timer.setSingleShot(true); timer.start(5000); while (timer.isActive() && m_server->m_connections.size() == origConns) qApp->processEvents(QEventLoop::AllEvents, 1000); QCOMPARE(m_testSocket.state(), QLocalSocket::ConnectedState); // Check that we've received the connections // One zeromq and one local socket ... #ifdef USE_ZERO_MQ QCOMPARE(m_server->m_connections.size(), 2); #else QCOMPARE(m_server->m_connections.size(), 1); #endif } void ServerTest::testClientDisconnected() { #ifdef USE_ZERO_MQ QCOMPARE(m_server->m_connections.size(), 2); #else QCOMPARE(m_server->m_connections.size(), 1); #endif int origConns = m_server->m_connections.size(); m_testSocket.disconnectFromServer(); // Wait 5 seconds for a timeout. QTimer timer; timer.setSingleShot(true); timer.start(5000); while (timer.isActive() && m_server->m_connections.size() == origConns) qApp->processEvents(QEventLoop::AllEvents, 1000); #ifdef USE_ZERO_MQ // The zero mq socket will be left ... QCOMPARE(m_server->m_connections.size(), 1); #else QCOMPARE(m_server->m_connections.size(), 0); #endif } // Simplify the addition of new validation tests. Expects files to exist in // molequeue/molequeue/testing/data/server-ref/ named: // - -request.json: a client request. // - -response.json: a reference server reply. void addValidation(const QString &name) { QTest::newRow(qPrintable(name)) << QString("server-ref/%1-request.json").arg(name) << QString("server-ref/%1-response.json").arg(name); } void ServerTest::handleMessage_data() { // Load testing jobs: m_server->jobManager()->loadJobState(MoleQueue_TESTDATA_DIR "server-ref"); QTest::addColumn("requestFile"); QTest::addColumn("responseFile"); // Invalid method addValidation("invalidMethod"); // listQueues addValidation("listQueues"); // submitJob addValidation("submitJob-paramsNotObject"); addValidation("submitJob-queueMissing"); addValidation("submitJob-programMissing"); addValidation("submitJob-queueNotString"); addValidation("submitJob-programNotString"); addValidation("submitJob-queueDoesNotExist"); addValidation("submitJob-programDoesNotExist"); addValidation("submitJob"); // cancelJob addValidation("cancelJob-paramsNotObject"); addValidation("cancelJob-moleQueueIdMissing"); addValidation("cancelJob-moleQueueIdInvalid"); addValidation("cancelJob-jobNotRunning"); addValidation("cancelJob-invalidQueue"); addValidation("cancelJob"); // lookupJob addValidation("lookupJob-paramsNotObject"); addValidation("lookupJob-moleQueueIdMissing"); addValidation("lookupJob-moleQueueIdInvalid"); addValidation("lookupJob"); // registerOpenWith addValidation("registerOpenWith"); addValidation("registerOpenWith-rpc"); addValidation("registerOpenWith-duplicateName"); // Must follow registerOpenWith addValidation("registerOpenWith-paramsNotObject"); addValidation("registerOpenWith-badNameExec"); addValidation("registerOpenWith-emptyName"); addValidation("registerOpenWith-patternsNotArray"); addValidation("registerOpenWith-patternNotObject"); addValidation("registerOpenWith-invalidPatternType"); // listOpenWithName addValidation("listOpenWithNames"); // unregisterOpenWith addValidation("unregisterOpenWith-prepare"); // add a dummy handler, and addValidation("unregisterOpenWith"); // remove it. addValidation("unregisterOpenWith-paramsNotObject"); addValidation("unregisterOpenWith-nameNotString"); } void ServerTest::handleMessage() { // Fetch the filenames for this iteration QFETCH(QString, requestFile); QFETCH(QString, responseFile); // Load the json strings ReferenceString requestString(requestFile); ReferenceString responseString(responseFile); // Parse the request into a message DummyConnection conn; QJsonDocument doc = QJsonDocument::fromJson(requestString.toString().toLatin1()); QVERIFY(doc.isObject()); Message message(doc.object(), &conn); QVERIFY(message.parse()); // Pass the message to the server for handling: m_server->handleMessage(message); // Verify that a reply was sent: QVERIFY(conn.messageCount() > 0); // Compare the reply with the reference reply QCOMPARE(QString(conn.popMessage().toJson()), responseString.toString()); } // Verify that the action factories added by the registerOpenWith test are valid void ServerTest::verifyOpenWithHandler() { // Get handlers: ActionFactoryManager *afm = ActionFactoryManager::instance(); QList factories = afm->factoriesOfType(); QCOMPARE(factories.size(), 2); // test the executable handler's configuration OpenWithActionFactory *spiffyClient = factories.first(); QVERIFY(spiffyClient != NULL); QCOMPARE(spiffyClient->name(), QString("My Spiffy Client")); QCOMPARE(spiffyClient->executable(), QString("client")); QList patterns = spiffyClient->filePatterns(); QCOMPARE(patterns.size(), 2); QCOMPARE(patterns[0].pattern(), QString("spiff[\\d]*\\.(?:dat|out)")); QCOMPARE(patterns[0].patternSyntax(), QRegExp::RegExp2); QCOMPARE(patterns[0].caseSensitivity(), Qt::CaseSensitive); QCOMPARE(patterns[1].pattern(), QString("*.spiffyout")); QCOMPARE(patterns[1].patternSyntax(), QRegExp::WildcardUnix); QCOMPARE(patterns[1].caseSensitivity(), Qt::CaseInsensitive); // test the rpc handler's configuration spiffyClient = factories.at(1); QVERIFY(spiffyClient != NULL); QCOMPARE(spiffyClient->name(), QString("My Spiffy Client (RPC)")); QCOMPARE(spiffyClient->rpcServer(), QString("rpc-client")); patterns = spiffyClient->filePatterns(); QCOMPARE(patterns.size(), 2); QCOMPARE(patterns[0].pattern(), QString("rpcspiff[\\d]*\\.(?:dat|out)")); QCOMPARE(patterns[0].patternSyntax(), QRegExp::RegExp2); QCOMPARE(patterns[0].caseSensitivity(), Qt::CaseSensitive); QCOMPARE(patterns[1].pattern(), QString("rpc*.spiffyout")); QCOMPARE(patterns[1].patternSyntax(), QRegExp::WildcardUnix); QCOMPARE(patterns[1].caseSensitivity(), Qt::CaseInsensitive); } QTEST_MAIN(ServerTest) #include "servertest.moc" molequeue-0.9.0/molequeue/app/testing/sgetest.cpp000066400000000000000000000123011323436134600221430ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include "queues/sge.h" class QueueSgeTest : public QObject { Q_OBJECT private: MoleQueue::QueueSge m_queue; private slots: /// Called before the first test function is executed. void initTestCase(); /// Called after the last test function is executed. void cleanupTestCase(); /// Called before each test function is executed. void init(); /// Called after every test function. void cleanup(); void sanityCheck(); void testParseJobId(); void testParseQueueLine(); }; void QueueSgeTest::initTestCase() { } void QueueSgeTest::cleanupTestCase() { } void QueueSgeTest::init() { } void QueueSgeTest::cleanup() { } void QueueSgeTest::sanityCheck() { QCOMPARE(m_queue.typeName(), QString("Sun Grid Engine")); QString testString = "some.host.somewhere"; m_queue.setHostName(testString); QCOMPARE(m_queue.hostName(), testString); testString = "aUser"; m_queue.setUserName(testString); QCOMPARE(m_queue.userName(), testString); m_queue.setSshPort(6887); QCOMPARE(m_queue.sshPort(), 6887); testString = "/some/path"; m_queue.setWorkingDirectoryBase(testString); QCOMPARE(m_queue.workingDirectoryBase(), testString); testString = "subComm"; m_queue.setSubmissionCommand(testString); QCOMPARE(m_queue.submissionCommand(), testString); testString = "reqComm"; m_queue.setRequestQueueCommand(testString); QCOMPARE(m_queue.requestQueueCommand(), testString); } void QueueSgeTest::testParseJobId() { QString submissionOutput = "your job 1235 ('someFile') has been submitted"; MoleQueue::IdType jobId; QVERIFY(m_queue.parseQueueId(submissionOutput, &jobId)); QCOMPARE(jobId, static_cast(1235)); } void QueueSgeTest::testParseQueueLine() { QString line; MoleQueue::IdType jobId; MoleQueue::JobState state; // First some invalid lines line = "job-ID prior name user state submit/start at queue function"; QVERIFY(!m_queue.parseQueueLine(line, &jobId, &state)); line = " 20:27:15"; QVERIFY(!m_queue.parseQueueLine(line, &jobId, &state)); line = "230 0 hydra craig inv 07/13/96 durin.q MASTER"; QVERIFY(!m_queue.parseQueueLine(line, &jobId, &state)); // Check various states: line = "231 0 hydra craig r 07/13/96 durin.q MASTER"; QVERIFY(m_queue.parseQueueLine(line, &jobId, &state)); QCOMPARE(jobId, static_cast(231)); QCOMPARE(state, MoleQueue::RunningRemote); line = "232 0 hydra craig d 07/13/96 durin.q MASTER"; QVERIFY(m_queue.parseQueueLine(line, &jobId, &state)); QCOMPARE(jobId, static_cast(232)); QCOMPARE(state, MoleQueue::RunningRemote); line = "233 0 hydra craig e 07/13/96 durin.q MASTER"; QVERIFY(m_queue.parseQueueLine(line, &jobId, &state)); QCOMPARE(jobId, static_cast(233)); QCOMPARE(state, MoleQueue::RunningRemote); line = "234 0 hydra craig qw 07/13/96 durin.q MASTER"; QVERIFY(m_queue.parseQueueLine(line, &jobId, &state)); QCOMPARE(jobId, static_cast(234)); QCOMPARE(state, MoleQueue::QueuedRemote); line = "235 0 hydra craig q 07/13/96 durin.q MASTER"; QVERIFY(m_queue.parseQueueLine(line, &jobId, &state)); QCOMPARE(jobId, static_cast(235)); QCOMPARE(state, MoleQueue::QueuedRemote); line = "236 0 hydra craig w 07/13/96 durin.q MASTER"; QVERIFY(m_queue.parseQueueLine(line, &jobId, &state)); QCOMPARE(jobId, static_cast(236)); QCOMPARE(state, MoleQueue::QueuedRemote); line = "237 0 hydra craig s 07/13/96 durin.q MASTER"; QVERIFY(m_queue.parseQueueLine(line, &jobId, &state)); QCOMPARE(jobId, static_cast(237)); QCOMPARE(state, MoleQueue::QueuedRemote); line = "238 0 hydra craig h 07/13/96 durin.q MASTER"; QVERIFY(m_queue.parseQueueLine(line, &jobId, &state)); QCOMPARE(jobId, static_cast(238)); QCOMPARE(state, MoleQueue::QueuedRemote); line = "239 0 hydra craig t 07/13/96 durin.q MASTER"; QVERIFY(m_queue.parseQueueLine(line, &jobId, &state)); QCOMPARE(jobId, static_cast(239)); QCOMPARE(state, MoleQueue::QueuedRemote); } QTEST_MAIN(QueueSgeTest) #include "sgetest.moc" molequeue-0.9.0/molequeue/app/testing/slurmtest.cpp000066400000000000000000000126611323436134600225400ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2013 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include "queues/slurm.h" class QueueSlurmTest : public QObject { Q_OBJECT private: MoleQueue::QueueSlurm m_queue; private slots: /// Called before the first test function is executed. void initTestCase(); /// Called after the last test function is executed. void cleanupTestCase(); /// Called before each test function is executed. void init(); /// Called after every test function. void cleanup(); void sanityCheck(); void testParseJobId(); void testParseQueueLine_data(); void testParseQueueLine(); }; void QueueSlurmTest::initTestCase() { } void QueueSlurmTest::cleanupTestCase() { } void QueueSlurmTest::init() { } void QueueSlurmTest::cleanup() { } void QueueSlurmTest::sanityCheck() { QCOMPARE(m_queue.typeName(), QString("SLURM")); QString testString = "some.host.somewhere"; m_queue.setHostName(testString); QCOMPARE(m_queue.hostName(), testString); testString = "aUser"; m_queue.setUserName(testString); QCOMPARE(m_queue.userName(), testString); m_queue.setSshPort(6887); QCOMPARE(m_queue.sshPort(), 6887); testString = "/some/path"; m_queue.setWorkingDirectoryBase(testString); QCOMPARE(m_queue.workingDirectoryBase(), testString); testString = "subComm"; m_queue.setSubmissionCommand(testString); QCOMPARE(m_queue.submissionCommand(), testString); testString = "reqComm"; m_queue.setRequestQueueCommand(testString); QCOMPARE(m_queue.requestQueueCommand(), testString); } void QueueSlurmTest::testParseJobId() { QString submissionOutput = "Submitted batch job 1234"; MoleQueue::IdType jobId; QVERIFY(m_queue.parseQueueId(submissionOutput, &jobId)); QCOMPARE(jobId, static_cast(1234)); submissionOutput = "Submitted batch job 12345\n"; QVERIFY(m_queue.parseQueueId(submissionOutput, &jobId)); QCOMPARE(jobId, static_cast(12345)); } void QueueSlurmTest::testParseQueueLine_data() { QTest::addColumn("data"); QTest::addColumn("canParse"); QTest::addColumn("jobId"); QTest::addColumn("state"); QTest::newRow("Header") << "JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)" << false << MoleQueue::InvalidId << MoleQueue::Unknown; QTest::newRow("Status: Cancelled, leading whitespace") << " 231 debug job2 dave CA 0:00 8 (Resources)" << true << static_cast(231) << MoleQueue::RunningRemote; QTest::newRow("Status: Cancelled, no leading whitespace") << "231 debug job2 dave CA 0:00 8 (Resources)" << true << static_cast(231) << MoleQueue::RunningRemote; QTest::newRow("Status: Completed") << "231 debug job2 dave CD 0:00 8 (Resources)" << true << static_cast(231) << MoleQueue::RunningRemote; QTest::newRow("Status: Configuring") << "231 debug job2 dave CF 0:00 8 (Resources)" << true << static_cast(231) << MoleQueue::QueuedRemote; QTest::newRow("Status: Completing") << "231 debug job2 dave CG 0:00 8 (Resources)" << true << static_cast(231) << MoleQueue::RunningRemote; QTest::newRow("Status: Failed") << "231 debug job2 dave F 0:00 8 (Resources)" << true << static_cast(231) << MoleQueue::RunningRemote; QTest::newRow("Status: Node fail") << "231 debug job2 dave NF 0:00 8 (Resources)" << true << static_cast(231) << MoleQueue::RunningRemote; QTest::newRow("Status: Pending") << "231 debug job2 dave PD 0:00 8 (Resources)" << true << static_cast(231) << MoleQueue::QueuedRemote; QTest::newRow("Status: Running") << "231 debug job2 dave R 0:00 8 (Resources)" << true << static_cast(231) << MoleQueue::RunningRemote; QTest::newRow("Status: Suspended") << "231 debug job2 dave R 0:00 8 (Resources)" << true << static_cast(231) << MoleQueue::RunningRemote; QTest::newRow("Status: Timeout") << "231 debug job2 dave TO 0:00 8 (Resources)" << true << static_cast(231) << MoleQueue::RunningRemote; } void QueueSlurmTest::testParseQueueLine() { QFETCH(QString, data); QFETCH(bool, canParse); QFETCH(MoleQueue::IdType, jobId); QFETCH(MoleQueue::JobState, state); MoleQueue::IdType parsedJobId; MoleQueue::JobState parsedState; QCOMPARE(m_queue.parseQueueLine(data, &parsedJobId, &parsedState), canParse); QCOMPARE(parsedJobId, jobId); QCOMPARE(parsedState, state); } QTEST_MAIN(QueueSlurmTest) #include "slurmtest.moc" molequeue-0.9.0/molequeue/app/testing/sshcommandtest.cpp000066400000000000000000000070401323436134600235250ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include "dummysshcommand.h" class SshCommandTest : public QObject { Q_OBJECT private: DummySshCommand m_ssh; private slots: /// Called before the first test function is executed. void initTestCase(); /// Called after the last test function is executed. void cleanupTestCase(); /// Called before each test function is executed. void init(); /// Called after every test function. void cleanup(); void sanityCheck(); void testExecute(); void testCopyTo(); void testCopyFrom(); void testCopyDirTo(); void testCopyDirFrom(); }; void SshCommandTest::initTestCase() { } void SshCommandTest::cleanupTestCase() { } void SshCommandTest::init() { m_ssh.setSshCommand("ssh"); m_ssh.setScpCommand("scp"); m_ssh.setHostName("host"); m_ssh.setUserName("user"); m_ssh.setPortNumber(22); } void SshCommandTest::cleanup() { } void SshCommandTest::sanityCheck() { m_ssh.setSshCommand("mySsh"); QCOMPARE(m_ssh.sshCommand(), QString("mySsh")); m_ssh.setScpCommand("myScp"); QCOMPARE(m_ssh.scpCommand(), QString("myScp")); m_ssh.setData(QVariant("Test")); QCOMPARE(m_ssh.data().toString(), QString("Test")); } void SshCommandTest::testExecute() { m_ssh.execute("ls ~"); QCOMPARE(m_ssh.getDummyCommand(), QString("ssh")); QCOMPARE(m_ssh.getDummyArgs(), QStringList () << QString("-q") << QString("user@host") << QString("ls ~")); } void SshCommandTest::testCopyTo() { m_ssh.copyTo("C:/local/path", "/remote/path"); QCOMPARE(m_ssh.getDummyCommand(), QString("scp")); QCOMPARE(m_ssh.getDummyArgs(), QStringList () << QString("-q") << QString("-S") << QString("ssh") << QString("C:/local/path") << QString("user@host:/remote/path")); } void SshCommandTest::testCopyFrom() { m_ssh.copyFrom("/remote/path", "C:/local/path"); QCOMPARE(m_ssh.getDummyCommand(), QString("scp")); QCOMPARE(m_ssh.getDummyArgs(), QStringList () << QString("-q") << QString("-S") << QString("ssh") << QString("user@host:/remote/path") << QString("C:/local/path")); } void SshCommandTest::testCopyDirTo() { m_ssh.copyDirTo("C:/local/path", "/remote/path"); QCOMPARE(m_ssh.getDummyCommand(), QString("scp")); QCOMPARE(m_ssh.getDummyArgs(), QStringList () << QString("-q") << QString("-S") << QString("ssh") << QString("-r") << QString("C:/local/path") << QString("user@host:/remote/path")); } void SshCommandTest::testCopyDirFrom() { m_ssh.copyDirFrom("/remote/path", "C:/local/path"); QCOMPARE(m_ssh.getDummyCommand(), QString("scp")); QCOMPARE(m_ssh.getDummyArgs(), QStringList () << QString("-q") << QString("-S") << QString("ssh") << QString("-r") << QString("user@host:/remote/path") << QString("C:/local/path")); } QTEST_MAIN(SshCommandTest) #include "sshcommandtest.moc" molequeue-0.9.0/molequeue/app/testing/testserver.cpp000066400000000000000000000021701323436134600226760ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "testserver.h" using namespace MoleQueue; TestServer::TestServer(PacketType *target) : QObject(NULL), m_target(target), m_server(new QLocalServer), m_socket(NULL) { if (!m_server->listen(getRandomSocketName())) { qWarning() << "Cannot start test server:" << m_server->errorString(); return; } connect(m_server, SIGNAL(newConnection()), this, SLOT(newConnection())); } TestServer::~TestServer() { if (m_socket != NULL) m_socket->disconnect(); delete m_server; } molequeue-0.9.0/molequeue/app/testing/testserver.h000066400000000000000000000061331323436134600223460ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef TESTSERVER_H #define TESTSERVER_H #include #include #include #include #include #include #include #include #include #include class TestServer : public QObject { Q_OBJECT MoleQueue::PacketType *m_target; QLocalServer *m_server; QLocalSocket *m_socket; public: TestServer(MoleQueue::PacketType *target); ~TestServer(); void sendPacket(const MoleQueue::PacketType &packet) { QDataStream out (m_socket); out.setVersion(QDataStream::Qt_4_7); out << packet; m_socket->flush(); } bool waitForConnection(int timeout_ms = 5000) { QTimer timer; timer.setSingleShot(true); timer.start(timeout_ms); while (timer.isActive() && m_socket == NULL) { qApp->processEvents(QEventLoop::AllEvents, 500); } return m_socket != NULL; } bool waitForPacket(int timeout_ms = 5000) { QTimer timer; timer.setSingleShot(true); timer.start(timeout_ms); while (timer.isActive() && m_target->isEmpty()) { qApp->processEvents(QEventLoop::AllEvents, 500); } return !m_target->isEmpty(); } QString socketName() const {return m_server->serverName();} static QString getRandomSocketName() { // Generate a time, process, and thread independent random value. quint32 threadPtr = static_cast( reinterpret_cast(QThread::currentThread())); quint32 procId = static_cast(qApp->applicationPid()); quint32 msecs = static_cast( QDateTime::currentDateTime().toMSecsSinceEpoch()); unsigned int seed = static_cast( (threadPtr ^ procId) ^ ((msecs << 16) ^ msecs)); qDebug() << "Seed:" << seed; qsrand(seed); int randVal = qrand(); return QString("MoleQueue-testing-%1").arg(QString::number(randVal)); } private slots: void newConnection() { m_socket = m_server->nextPendingConnection(); connect(m_socket, SIGNAL(disconnected()), m_socket, SLOT(deleteLater())); connect(m_socket, SIGNAL(readyRead()), this, SLOT(readyRead())); } void readyRead() { // qDebug() << "Test server received" << m_socket->bytesAvailable() << "bytes."; QDataStream in (m_socket); in.setVersion(QDataStream::Qt_4_7); MoleQueue::PacketType packet; in >> *m_target; } }; #endif // TESTSERVER_H molequeue-0.9.0/molequeue/app/testing/uittest.cpp000066400000000000000000000062411323436134600221740ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include #include #include #include #include #include #include "queues/uit/sslsetup.h" #include "queues/queueuit.h" #include "dummyserver.h" #include "xmlutils.h" #include "referencestring.h" #include "queues/uit/jobeventlist.h" #include "jobmanager.h" class QueueUitTest : public QObject { Q_OBJECT private slots: void testSslSetup(); void testJobIdRegex(); void testHandleQueueUpdate(); }; void QueueUitTest::testSslSetup() { qRegisterMetaType >("QList"); MoleQueue::Uit::SslSetup::init(); QSslSocket socket; // Does our Qt have SSL support? QVERIFY(socket.supportsSsl()); socket.connectToHostEncrypted( "www.uit.hpc.mil", 443 ); socket.write( "GET / HTTP/1.1\r\n" \ "Host: www.uit.hpc.mil\r\n" \ "Connection: Close\r\n\r\n" ); QSignalSpy spy (&socket, SIGNAL(sslErrors(const QList&))); while ( socket.waitForReadyRead() ) { qDebug() << socket.readAll().data(); } // If SSL certificates are setup correctly we shouldn't get any errors ... QCOMPARE(spy.count(), 0); } void QueueUitTest::testJobIdRegex() { QString submissionOutput = "Job <75899> is submitted to debug queue."; QRegExp parser ("^Job <(\\d+)> .*"); parser.indexIn(submissionOutput); QCOMPARE(parser.cap(1), QString("75899")); } void QueueUitTest::testHandleQueueUpdate() { DummyServer server; MoleQueue::JobManager *jobManager = server.jobManager(); MoleQueue::Job jobQueuedRemote = jobManager->newJob(); jobQueuedRemote.setMoleQueueId(100535); jobQueuedRemote.setQueueId(100535); MoleQueue::Job jobRunningRemote = jobManager->newJob(); jobRunningRemote.setMoleQueueId(100536); jobRunningRemote.setQueueId(100536); MoleQueue::QueueUit queue(server.queueManager()); queue.m_jobs[100535] = 100535; queue.m_jobs[100536] = 100536; QString m_jobEventXml = XmlUtils::stripWhitespace( ReferenceString("uit-ref/jobeventlist.xml")); MoleQueue::Uit::JobEventList list = MoleQueue::Uit::JobEventList::fromXml(m_jobEventXml); QVERIFY(jobQueuedRemote.jobState() != MoleQueue::QueuedRemote); QVERIFY(jobRunningRemote.jobState() != MoleQueue::RunningRemote); queue.handleQueueUpdate(list.jobEvents()); QVERIFY(jobQueuedRemote.jobState() == MoleQueue::QueuedRemote); QVERIFY(jobRunningRemote.jobState() == MoleQueue::RunningRemote); } QTEST_MAIN(QueueUitTest) #include "uittest.moc" molequeue-0.9.0/molequeue/app/testing/userhostassoclisttest.cpp000066400000000000000000000043011323436134600251670ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include #include #include "queues/uit/userhostassoclist.h" #include "referencestring.h" #include "xmlutils.h" class UserHostAssocListTest : public QObject { Q_OBJECT private slots: void initTestCase(); void testFromXml(); private: QString m_userHostAssocXml; }; void UserHostAssocListTest::initTestCase() { m_userHostAssocXml = XmlUtils::stripWhitespace( ReferenceString("userhostassoclist-ref/userhostassoclisttest.xml")); } void UserHostAssocListTest::testFromXml() { MoleQueue::Uit::UserHostAssocList userHostAssocList = MoleQueue::Uit::UserHostAssocList::fromXml(m_userHostAssocXml); QCOMPARE(userHostAssocList.userHostAssocs().size(), 2); MoleQueue::Uit::UserHostAssoc userHostAssoc = userHostAssocList.userHostAssocs()[0]; QCOMPARE(userHostAssoc.account(), QString("user")); QVERIFY(userHostAssoc.hostId() == 2); QCOMPARE(userHostAssoc.transportMethod(), QString("MSRC_KERBEROS")); QCOMPARE(userHostAssoc.hostName(), QString("ruby.erdc.hpc.mil")); QCOMPARE(userHostAssoc.description(), QString("Origin 3900")); userHostAssoc = userHostAssocList.userHostAssocs()[1]; QCOMPARE(userHostAssoc.account(), QString("user")); QVERIFY(userHostAssoc.hostId() == 3); QCOMPARE(userHostAssoc.transportMethod(), QString("MSRC_KERBEROS")); QCOMPARE(userHostAssoc.hostName(), QString("lead.erdc.hpc.mil")); QCOMPARE(userHostAssoc.description(), QString("Origin 3901")); QCOMPARE(userHostAssoc.systemName(), QString("ERDC::DIAMOND")); } QTEST_MAIN(UserHostAssocListTest) #include "userhostassoclisttest.moc" molequeue-0.9.0/molequeue/app/testing/xmlutils.cpp000066400000000000000000000014541323436134600223550ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "xmlutils.h" #include QString & XmlUtils::stripWhitespace(QString &xml) { QRegExp whiteSpace(">\\s*<"); return xml.replace(whiteSpace, "><"); } molequeue-0.9.0/molequeue/app/testing/xmlutils.h000066400000000000000000000014461323436134600220230ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef XMLUTILS_H_ #define XMLUTILS_H_ #include class XmlUtils { public: static QString & stripWhitespace(QString &xml); }; #endif /* XMLUTILS_H_ */ molequeue-0.9.0/molequeue/app/ui/000077500000000000000000000000001323436134600167245ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/ui/aboutdialog.ui000066400000000000000000000216541323436134600215650ustar00rootroot00000000000000 AboutDialog 0 0 516 548 0 0 true 0 0 :/icons/MoleQueue_About.png Qt::Horizontal 40 20 <html><head/><body><p><span style=" font-size:20pt; font-weight:600;">Version:</span></p></body></html> Qt::AutoText Qt::AlignCenter <html><head/><body><p><span style=" font-size:20pt; font-weight:600;">0.1</span></p></body></html> Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse Qt::Horizontal 40 20 Qt::Horizontal 40 20 <html><head/><body><p><span style=" font-size:10pt; font-weight:600;">Qt Version:</span></p></body></html> <html><head/><body><p><span style=" font-size:10pt; font-weight:600;">0.1</span></p></body></html> Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse Qt::Horizontal 40 20 Qt::Vertical 20 5 Qt::Horizontal 40 20 <html><head/><body><p><span style=" font-size:10pt; font-weight:600;">License: </span><a href="http://opensource.org/licenses/BSD-3-Clause"><span style=" font-size:10pt; text-decoration: underline; color:#0000ff;">BSD-3-Clause</span></a></p></body></html> Qt::Horizontal 40 20 Qt::Vertical 20 5 <html><head/><body><p><a href="http://www.openchemistry.org"><span style=" text-decoration: underline; color:#0000ff;">www.openchemistry.org</span></a></p></body></html> true Qt::Horizontal 40 20 <html><head/><body><p><a href="http://www.kitware.com"><span style=" text-decoration: underline; color:#0000ff;">www.kitware.com</span></a></p></body></html> Qt::RichText Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter true 6 0 Qt::Horizontal 0 20 OK Image OK clicked() aboutDialog accept() 278 253 96 254 molequeue-0.9.0/molequeue/app/ui/addqueuedialog.ui000066400000000000000000000044451323436134600222470ustar00rootroot00000000000000 AddQueueDialog 0 0 340 115 Add Queue Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Name: Type: Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok nameLineEdit typeComboBox buttonBox buttonBox accepted() AddQueueDialog accept() 248 254 157 274 buttonBox rejected() AddQueueDialog reject() 316 260 286 274 molequeue-0.9.0/molequeue/app/ui/advancedfilterdialog.ui000066400000000000000000000141671323436134600234270ustar00rootroot00000000000000 AdvancedFilterDialog 0 0 495 217 MoleQueue Filter Options 0 0 Visible Job Statuses false Submitted Queued Error 0 0 None New Finished Canceled Running 0 0 All Qt::Horizontal 40 20 <html><head/><body><p>Toggle whether hidden jobs are displayed.</p><p>Clients that submit large numbers of automated jobs may wish to hide them from the default job view by setting the &quot;hideFromGui&quot; flag. This overrides the client request and includes such jobs in the table. </p></body></html> Show hidden jobs Qt::Horizontal QDialogButtonBox::Close filterStatusNew filterStatusSubmitted filterStatusQueued filterStatusRunning filterStatusFinished filterStatusCanceled filterStatusError filterStatusAll filterStatusNone filterShowHidden buttonBox buttonBox accepted() AdvancedFilterDialog accept() 248 254 157 274 buttonBox rejected() AdvancedFilterDialog reject() 316 260 286 274 molequeue-0.9.0/molequeue/app/ui/credentialsdialog.ui000066400000000000000000000062411323436134600227430ustar00rootroot00000000000000 CredentialsDialog 0 0 408 146 Credentials QFormLayout::AllNonFixedFieldsGrow Qt::AlignCenter QFormLayout::AllNonFixedFieldsGrow Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter QLineEdit::Password Password Qt::AutoText Qt::Vertical 379 4 Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok credentialsEdit buttonBox buttonBox accepted() CredentialsDialog accept() 248 254 157 274 buttonBox rejected() CredentialsDialog reject() 316 260 286 274 molequeue-0.9.0/molequeue/app/ui/importprogramdialog.ui000066400000000000000000000053301323436134600233460ustar00rootroot00000000000000 ImportProgramDialog 0 0 369 117 Import Program QFormLayout::ExpandingFieldsGrow Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Name: File: ... Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok nameEdit fileEdit fileButton buttonBox buttonBox accepted() ImportProgramDialog accept() 248 254 157 274 buttonBox rejected() ImportProgramDialog reject() 316 260 286 274 molequeue-0.9.0/molequeue/app/ui/importqueuedialog.ui000066400000000000000000000061471323436134600230320ustar00rootroot00000000000000 ImportQueueDialog 0 0 388 146 Import Queue Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Name: File: <html><head/><body><p>Import programs</p><p><br/></p><p>Import any program configurations stored in the file.</p></body></html> Import programs? ... Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok nameEdit fileEdit fileButton importPrograms buttonBox buttonBox accepted() ImportQueueDialog accept() 248 254 157 274 buttonBox rejected() ImportQueueDialog reject() 316 260 286 274 molequeue-0.9.0/molequeue/app/ui/jobtablewidget.ui000066400000000000000000000105631323436134600222560ustar00rootroot00000000000000 JobTableWidget 0 0 652 481 Form 0 0 Qt::DefaultContextMenu QAbstractItemView::ExtendedSelection QAbstractItemView::SelectRows 0 0 Filter: 0 0 <html><head/><body><p>Filter jobs from the table.</p><p>Each whitespace separated term will be used to restrict which jobs are displayed in the job table. Entries in the table that match terms here will be displayed. Terms beginning with '-' will negate the match.</p><p>For example, the filter &quot;water -opt&quot; will match a job with the title &quot;Water single point&quot;, but not &quot;Water geometry optimization&quot;. Matches are made against all visible text in the job entry.</p></body></html> true ... More... false false MoleQueue::JobView QTableView
jobview.h
filterEdit toolButton_3 filterMore table toolButton_3 clicked() filterEdit clear() 564 465 485 457
molequeue-0.9.0/molequeue/app/ui/localqueuewidget.ui000066400000000000000000000017701323436134600226330ustar00rootroot00000000000000 LocalQueueWidget 0 0 400 300 Form Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Number of available cores: 1 256 molequeue-0.9.0/molequeue/app/ui/logwindow.ui000066400000000000000000000006101323436134600212710ustar00rootroot00000000000000 LogWindow 0 0 600 300 MoleQueue Log molequeue-0.9.0/molequeue/app/ui/mainwindow.ui000066400000000000000000000123211323436134600214360ustar00rootroot00000000000000 MainWindow 0 0 600 300 MoleQueue 0 0 <html><head/><body><p><span style=" color:#ff0000;">An error has occurred. Click to <a href="viewLog">view the log</a> or <a href="clearError">remove this notification.</a></span></p></body></html> Qt::AlignCenter 0 0 600 23 &File &View &Jobs Help &Quit Ctrl+Q Mi&nimize Ma&ximize &Restore Queue &Manager "Open With" Manager... Configure "Open with" applications Show &Log Ctrl+L &Update remote queues Update the status of jobs on remote queuing systems. Ctrl+R true Job &Filter Show Job Filter Ctrl+F Clear &Finished Jobs Clear finished jobs from MoleQueue Ad&vanced Job Filters... Show advance job filtering options About MoleQueue::JobTableWidget QWidget
jobtablewidget.h
1
molequeue-0.9.0/molequeue/app/ui/openwithmanagerdialog.ui000066400000000000000000000326621323436134600236440ustar00rootroot00000000000000 OpenWithManagerDialog 0 0 763 480 "Open With" Manager true Recognized Output File Names: Qt::Horizontal Test filename: editTest <html><head/><body><p>Enter a example filename for this executable.</p><p>The text will turn green if a filter matches it or</p><p>red otherwise.</p></body></html> Qt::Horizontal Case sensitive Qt::Horizontal 40 20 Apply Revert Pattern: editPattern 0 0 0 0 Wildcard RegExp Add Qt::Horizontal 173 20 0 0 Remove QAbstractItemView::SingleSelection QAbstractItemView::SelectRows Qt::Horizontal QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok Executable name: Add Qt::Horizontal 185 20 Remove Qt::Horizontal Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Name: Type: Executable Remote Procedure Call (advanced) 0 0 QFormLayout::AllNonFixedFieldsGrow 0 0 Executable: editExec 0 0 ... QFormLayout::AllNonFixedFieldsGrow RPC Call: <html><head/><body><p>The RPC server and method to call, specified as &quot;method@server&quot;.</p></body></html> method@server QAbstractItemView::SingleSelection QAbstractItemView::SelectRows true tableFactories pushAddFactory pushRemoveFactory editName comboType editExec pushExec editRpc tablePattern pushAddPattern pushRemovePattern editPattern comboMatch checkCaseSensitive pushApplyPattern pushRevertPattern editTest buttonBox buttonBox accepted() OpenWithManagerDialog accept() 252 475 157 274 buttonBox rejected() OpenWithManagerDialog reject() 320 475 286 274 molequeue-0.9.0/molequeue/app/ui/programconfiguredialog.ui000066400000000000000000000161511323436134600240200ustar00rootroot00000000000000 ProgramConfigureDialog 0 0 986 546 Configure Program Qt::Horizontal QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok Input Template Qt::Horizontal 40 20 Launch &Syntax: combo_syntax &Customize 100 0 Qt::Horizontal 40 20 Template &Help 0 0 300 0 Settings QFormLayout::ExpandingFieldsGrow Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter &Name: edit_name E&xecutable: Arguments: &Output filename: edit_outputFilename Qt::StrongFocus MoleQueue::FileBrowseWidget QWidget
filebrowsewidget.h
1
edit_name edit_remoteExecutable browse_localExecutable edit_arguments edit_outputFilename combo_syntax push_customize text_launchTemplate templateHelpButton buttonBox buttonBox rejected() ProgramConfigureDialog reject() 316 260 286 274 buttonBox accepted() ProgramConfigureDialog accept() 248 254 157 274
molequeue-0.9.0/molequeue/app/ui/queuemanagerdialog.ui000066400000000000000000000066361323436134600231350ustar00rootroot00000000000000 QueueManagerDialog 0 0 475 291 Queue Manager QAbstractItemView::NoEditTriggers true QAbstractItemView::SingleSelection QAbstractItemView::SelectRows true true Add a new queue. &Add... false Configure and add programs to the selected queue. &Configure... false false Remove the selected queue. &Remove false Qt::Horizontal 40 20 Import... false Export... queueTable addQueueButton configureQueueButton removeQueueButton importQueueButton exportQueueButton molequeue-0.9.0/molequeue/app/ui/queuesettingsdialog.ui000066400000000000000000000151311323436134600233510ustar00rootroot00000000000000 QueueSettingsDialog 0 0 546 344 0 0 Queue Settings 0 Configuration Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Name: Type: Local 0 0 QFrame::StyledPanel QFrame::Raised Programs QAbstractItemView::NoEditTriggers true QAbstractItemView::SingleSelection QAbstractItemView::SelectRows true Add... false Configure... false Remove Qt::Horizontal 40 20 Import... false Export... QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok tabWidget nameLineEdit programsTable addProgramButton configureProgramButton removeProgramButton importProgramButton exportProgramButton buttonBox accepted() QueueSettingsDialog accept() 368 278 497 274 buttonBox rejected() QueueSettingsDialog reject() 362 287 0 250 molequeue-0.9.0/molequeue/app/ui/remotequeuewidget.ui000066400000000000000000000421771323436134600230420ustar00rootroot00000000000000 RemoteQueueWidget 0 0 974 566 Form 0 0 0 0 0 0 0 0 0 0 0 0 Queue configuration QFormLayout::ExpandingFieldsGrow Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 Job &submission command: edit_submissionCommand 150 0 Request &queue command: edit_requestQueueCommand &Launch script name: edit_launchScriptName Remote &working directory: edit_workingDirectoryBase Job &cancel command: edit_killCommand Default &walltime limit: wallTimeHours 0 0 h 2000 24 m 59 Queue update interval: 0 0 m 1 120 0 0 SSH configuration 0 9 9 9 0 &Basic QFormLayout::AllNonFixedFieldsGrow Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter &Hostname: editHostName &User: editUserName Qt::Horizontal 70 20 Test C&onnection... &Advanced Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter SS&H executable: sshExecutableEdit SCP &executable: scpExecutableEdit &Identity file (private key): editIdentityFile ... &Port: spinSshPort 1 Qt::Horizontal 40 20 Su&bmit test job... 0 0 0 0 0 0 Launcher template 0 9 9 0 Template &help Qt::Horizontal 40 20 0 0 400 400 text_launchTemplate templateHelpButton edit_submissionCommand edit_killCommand edit_requestQueueCommand updateIntervalSpin edit_launchScriptName edit_workingDirectoryBase wallTimeHours wallTimeMinutes sshTabWidget editHostName editUserName pushTestConnection sshExecutableEdit scpExecutableEdit editIdentityFile fileButton spinSshPort push_sleepTest text_launchTemplate templateHelpButton molequeue-0.9.0/molequeue/app/ui/templatekeyworddialog.ui000066400000000000000000000027321323436134600236670ustar00rootroot00000000000000 TemplateKeywordDialog 0 0 750 627 Template Help Qt::Horizontal 40 20 &Close pushButton clicked() TemplateKeywordDialog accept() 360 286 399 265 molequeue-0.9.0/molequeue/app/ui/uitqueuewidget.ui000066400000000000000000000232301323436134600223350ustar00rootroot00000000000000 UitQueueWidget 0 0 974 516 Form UIT connection settings QFormLayout::AllNonFixedFieldsGrow Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Kerberos Realm editKerberosRealm HPCMP.HPC.MIL &Kerberos Username editKerberosUserName Qt::Horizontal 40 20 Test C&onnection... 0 0 Queue configuration QFormLayout::ExpandingFieldsGrow Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Queue update interval: 0 0 m 1 120 Remote working directory: editWorkingDirectoryBase Default &walltime limit: wallTimeHours 0 0 h 2000 24 m 59 Qt::Horizontal 40 20 S&ubmit test job... Hostname: Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 0 0 190 0 QComboBox::AdjustToContents Get Hosts... 0 0 Launcher template Qt::Horizontal 40 20 Template &help 0 0 400 400 hostNameComboBox pushGetHostNames updateIntervalSpin editWorkingDirectoryBase wallTimeHours wallTimeMinutes pushSleepTest editKerberosRealm editKerberosUserName pushTestConnection text_launchTemplate templateHelpButton molequeue-0.9.0/molequeue/app/uitqueuewidget.cpp000066400000000000000000000160011323436134600220630ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "uitqueuewidget.h" #include "molequeueconfig.h" #include "ui_uitqueuewidget.h" #include "program.h" #include "queues/queueuit.h" #include "templatekeyworddialog.h" #include #include #include #include #include #include #include #include namespace MoleQueue { UitQueueWidget::UitQueueWidget(QueueUit *queue, QWidget *parentObject) : AbstractQueueSettingsWidget(parentObject), ui(new Ui::UitQueueWidget), m_queue(queue), m_client(NULL), m_helpDialog(NULL) { ui->setupUi(this); reset(); connect(ui->editWorkingDirectoryBase, SIGNAL(textChanged(QString)), this, SLOT(setDirty())); connect(ui->editKerberosUserName, SIGNAL(textChanged(QString)), this, SLOT(setDirty())); connect(ui->editKerberosRealm, SIGNAL(textChanged(QString)), this, SLOT(setDirty())); connect(ui->wallTimeHours, SIGNAL(valueChanged(int)), this, SLOT(setDirty())); connect(ui->wallTimeMinutes, SIGNAL(valueChanged(int)), this, SLOT(setDirty())); connect(ui->pushTestConnection, SIGNAL(clicked()), this, SLOT(testConnection())); connect(ui->pushSleepTest, SIGNAL(clicked()), this, SLOT(sleepTest())); connect(ui->templateHelpButton, SIGNAL(clicked()), this, SLOT(showHelpDialog())); connect(ui->pushGetHostNames, SIGNAL(clicked()), queue, SLOT(getUserHostAssoc())); connect(ui->hostNameComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(setDirty())); connect(queue, SIGNAL(userHostAssocList(const Uit::UserHostAssocList&)), this, SLOT(updateHostList(const Uit::UserHostAssocList&))); connect(ui->text_launchTemplate, SIGNAL(textChanged()), this, SLOT(setDirty())); } UitQueueWidget::~UitQueueWidget() { delete ui; } void UitQueueWidget::save() { m_queue->setWorkingDirectoryBase(ui->editWorkingDirectoryBase->text()); m_queue->setKerberosRealm(ui->editKerberosRealm->text()); m_queue->setKerberosUserName(ui->editKerberosUserName->text()); m_queue->setHostName(ui->hostNameComboBox->currentText()); int index = ui->hostNameComboBox->currentIndex(); m_queue->setHostID(ui->hostNameComboBox->itemData(index).toULongLong()); m_queue->setQueueUpdateInterval(ui->updateIntervalSpin->value()); QString text = ui->text_launchTemplate->document()->toPlainText(); m_queue->setLaunchTemplate(text); int hours = ui->wallTimeHours->value(); int minutes = ui->wallTimeMinutes->value(); m_queue->setDefaultMaxWallTime(minutes + (hours * 60)); setDirty(false); } void UitQueueWidget::reset() { ui->editWorkingDirectoryBase->setText(m_queue->workingDirectoryBase()); ui->updateIntervalSpin->setValue(m_queue->queueUpdateInterval()); ui->editKerberosRealm->setText(m_queue->kerberosRealm()); ui->editKerberosUserName->setText(m_queue->kerberosUserName()); if (ui->hostNameComboBox->count() > 0) { int index = ui->hostNameComboBox->findText(m_queue->hostName()); if (index == -1) index = 0; ui->hostNameComboBox->setCurrentIndex(index); } else { ui->hostNameComboBox->addItem(m_queue->hostName(), m_queue->hostId()); } ui->text_launchTemplate->document()->setPlainText(m_queue->launchTemplate()); setDirty(false); } void UitQueueWidget::testConnection() { m_queue->testConnection(this); } void UitQueueWidget::sleepTest() { #ifdef MoleQueue_BUILD_CLIENT QString promptString; if (isDirty()) { promptString = tr("Would you like to apply the current settings and submit " "a test job? The job will run 'sleep 30' on the remote " "queue."); } else { promptString = tr("Would you like to submit a test job? The job will run " "'sleep 30' on the remote queue."); } QMessageBox::StandardButton response = QMessageBox::question(this, tr("Submit test job?"), promptString, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); if (response != QMessageBox::Yes) return; if (isDirty()) save(); // Check that important variables are set: QString missingVariable = ""; if (m_queue->hostName().isEmpty()) missingVariable = tr("server hostname"); else if (m_queue->kerberosUserName().isEmpty()) missingVariable = tr("kerberos username"); else if (m_queue->kerberosRealm().isEmpty()) missingVariable = tr("kerberos realm"); else if (m_queue->workingDirectoryBase().isEmpty()) missingVariable = tr("remote working directory"); if (!missingVariable.isEmpty()) { QMessageBox::critical(this, tr("Missing information"), tr("Refusing to test job submission: %1 not set.") .arg(missingVariable)); return; } Program *sleepProgram = m_queue->lookupProgram("sleep (testing)"); if (sleepProgram == NULL) { // Add sleep if it's not present sleepProgram = new Program (m_queue); sleepProgram->setName("sleep (testing)"); sleepProgram->setArguments("30"); sleepProgram->setExecutable("sleep"); sleepProgram->setOutputFilename(""); sleepProgram->setLaunchSyntax(Program::PLAIN); m_queue->addProgram(sleepProgram); } if (!m_client) { m_client = new Client (this); m_client->connectToServer(); } JobObject sleepJob; sleepJob.setQueue(m_queue->name()); sleepJob.setProgram(sleepProgram->name()); sleepJob.setDescription("sleep 30 (test)"); m_client->submitJob(sleepJob); #endif // MoleQueue_BUILD_CLIENT } void UitQueueWidget::showHelpDialog() { if (!m_helpDialog) m_helpDialog = new TemplateKeywordDialog(this); m_helpDialog->show(); } void UitQueueWidget::updateHostList(const Uit::UserHostAssocList &list) { QList hostAssocs = list.userHostAssocs(); QString currentHost = ui->hostNameComboBox->currentText(); ui->hostNameComboBox->clear(); foreach(const Uit::UserHostAssoc hostAssoc, hostAssocs) { ui->hostNameComboBox->addItem(hostAssoc.hostName(), hostAssoc.hostId()); } int index = ui->hostNameComboBox->findText(currentHost); if (index != -1) ui->hostNameComboBox->setCurrentIndex(index); else { ui->hostNameComboBox->insertItem(0, "Select Hostname ..."); ui->hostNameComboBox->setCurrentIndex(0); } } } // end namespace MoleQueue molequeue-0.9.0/molequeue/app/uitqueuewidget.h000066400000000000000000000031721323436134600215350ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef UITQUEUEWIDGET_H #define UITQUEUEWIDGET_H #include "abstractqueuesettingswidget.h" #include "queues/uit/userhostassoclist.h" namespace Ui { class UitQueueWidget; } namespace MoleQueue { class Client; class QueueUit; class TemplateKeywordDialog; /** * @class UitQueueWidget uitqueuewidget.h * * @brief A configuration dialog for UIT queuing systems. */ class UitQueueWidget: public AbstractQueueSettingsWidget { Q_OBJECT public: explicit UitQueueWidget(QueueUit *queue, QWidget *parentObject = 0); ~UitQueueWidget(); public slots: void save(); void reset(); protected slots: void testConnection(); void sleepTest(); void showHelpDialog(); void updateHostList(const Uit::UserHostAssocList &list); private slots: //void showFileDialog(); private: Ui::UitQueueWidget *ui; QueueUit *m_queue; Client *m_client; // Used for submitting test jobs. TemplateKeywordDialog *m_helpDialog; }; } // end namespace MoleQueue #endif //UITQUEUEWIDGET_H molequeue-0.9.0/molequeue/app/wsdl/000077500000000000000000000000001323436134600172605ustar00rootroot00000000000000molequeue-0.9.0/molequeue/app/wsdl/uitapi.wsdl000066400000000000000000001604161323436134600214560ustar00rootroot00000000000000 molequeue-0.9.0/molequeue/client/000077500000000000000000000000001323436134600170055ustar00rootroot00000000000000molequeue-0.9.0/molequeue/client/CMakeLists.txt000066400000000000000000000016441323436134600215520ustar00rootroot00000000000000find_package(Qt5 COMPONENTS Network REQUIRED) set(sources client.cpp jobobject.cpp jsonrpcclient.cpp ) set(headers client.h jobobject.h jsonrpcclient.h ) include_directories(${CMAKE_CURRENT_BINARY_DIR}) add_library(MoleQueueClient ${sources}) qt5_use_modules(MoleQueueClient Network) set_target_properties(MoleQueueClient PROPERTIES AUTOMOC TRUE) include(GenerateExportHeader) generate_export_header(MoleQueueClient EXPORT_FILE_NAME molequeueclientexport.h) list(APPEND headers "${CMAKE_CURRENT_BINARY_DIR}/molequeueclientexport.h") set_property(TARGET MoleQueueClient APPEND PROPERTY COMPILE_FLAGS ${molequeue_export_flags}) install(FILES ${headers} DESTINATION "${INSTALL_INCLUDE_DIR}/molequeue/client") install(TARGETS MoleQueueClient EXPORT "MoleQueueTargets" RUNTIME DESTINATION ${INSTALL_RUNTIME_DIR} LIBRARY DESTINATION ${INSTALL_LIBRARY_DIR} ARCHIVE DESTINATION ${INSTALL_ARCHIVE_DIR} ) molequeue-0.9.0/molequeue/client/client.cpp000066400000000000000000000211361323436134600207720ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012-2013 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "client.h" #include "jsonrpcclient.h" #include "jobobject.h" #include namespace MoleQueue { Client::Client(QObject *parent_) : QObject(parent_), m_jsonRpcClient(NULL) { } Client::~Client() { } bool Client::isConnected() const { if (!m_jsonRpcClient) return false; else return m_jsonRpcClient->isConnected(); } bool Client::connectToServer(const QString &serverName) { if (!m_jsonRpcClient) { m_jsonRpcClient = new JsonRpcClient(this); connect(m_jsonRpcClient, SIGNAL(resultReceived(QJsonObject)), SLOT(processResult(QJsonObject))); connect(m_jsonRpcClient, SIGNAL(notificationReceived(QJsonObject)), SLOT(processNotification(QJsonObject))); connect(m_jsonRpcClient, SIGNAL(errorReceived(QJsonObject)), SLOT(processError(QJsonObject))); connect(m_jsonRpcClient, SIGNAL(connectionStateChanged()), SIGNAL(connectionStateChanged())); } return m_jsonRpcClient->connectToServer(serverName); } int Client::requestQueueList() { if (!m_jsonRpcClient) return -1; QJsonObject packet = m_jsonRpcClient->emptyRequest(); packet["method"] = QLatin1String("listQueues"); if (!m_jsonRpcClient->sendRequest(packet)) return -1; int localId = static_cast(packet["id"].toDouble()); m_requests[localId] = ListQueues; return localId; } int Client::submitJob(const JobObject &job) { if (!m_jsonRpcClient) return -1; QJsonObject packet = m_jsonRpcClient->emptyRequest(); packet["method"] = QLatin1String("submitJob"); packet["params"] = job.json(); if (!m_jsonRpcClient->sendRequest(packet)) return -1; int localId = static_cast(packet["id"].toDouble()); m_requests[localId] = SubmitJob; return localId; } int Client::lookupJob(unsigned int moleQueueId) { if (!m_jsonRpcClient) return -1; QJsonObject packet = m_jsonRpcClient->emptyRequest(); packet["method"] = QLatin1String("lookupJob"); QJsonObject params; params["moleQueueId"] = static_cast(moleQueueId); packet["params"] = params; if (!m_jsonRpcClient->sendRequest(packet)) return -1; int localId = static_cast(packet["id"].toDouble()); m_requests[localId] = LookupJob; return localId; } int Client::cancelJob(unsigned int moleQueueId) { if (!m_jsonRpcClient) return -1; QJsonObject packet = m_jsonRpcClient->emptyRequest(); packet["method"] = QLatin1String("cancelJob"); QJsonObject params; params["moleQueueId"] = static_cast(moleQueueId); packet["params"] = params; if (!m_jsonRpcClient->sendRequest(packet)) return -1; int localId = static_cast(packet["id"].toDouble()); m_requests[localId] = CancelJob; return localId; } int Client::registerOpenWith(const QString &name, const QString &executable, const QList &filePatterns) { if (!m_jsonRpcClient) return -1; QJsonObject method; method["executable"] = executable; QJsonObject packet(buildRegisterOpenWithRequest(name, filePatterns, method)); if (!m_jsonRpcClient->sendRequest(packet)) return -1; int localId = static_cast(packet["id"].toDouble()); m_requests[localId] = RegisterOpenWith; return localId; } int Client::registerOpenWith(const QString &name, const QString &rpcServer, const QString &rpcMethod, const QList &filePatterns) { if (!m_jsonRpcClient) return -1; QJsonObject method; method["rpcServer"] = rpcServer; method["rpcMethod"] = rpcMethod; QJsonObject packet(buildRegisterOpenWithRequest(name, filePatterns, method)); if (!m_jsonRpcClient->sendRequest(packet)) return -1; int localId = static_cast(packet["id"].toDouble()); m_requests[localId] = RegisterOpenWith; return localId; } int Client::listOpenWithNames() { if (!m_jsonRpcClient) return -1; QJsonObject packet = m_jsonRpcClient->emptyRequest(); packet["method"] = QLatin1String("listOpenWithNames"); if (!m_jsonRpcClient->sendRequest(packet)) return -1; int localId = static_cast(packet["id"].toDouble()); m_requests[localId] = ListOpenWithNames; return localId; } int Client::unregisterOpenWith(const QString &handlerName) { if (!m_jsonRpcClient) return -1; QJsonObject packet = m_jsonRpcClient->emptyRequest(); packet["method"] = QLatin1String("unregisterOpenWith"); QJsonObject params; params["name"] = handlerName; packet["params"] = params; if (!m_jsonRpcClient->sendRequest(packet)) return -1; int localId = static_cast(packet["id"].toDouble()); m_requests[localId] = UnregisterOpenWith; return localId; } void Client::flush() { if (m_jsonRpcClient) m_jsonRpcClient->flush(); } void Client::processResult(const QJsonObject &response) { if (response["id"] != QJsonValue::Null && m_requests.contains(static_cast(response["id"].toDouble()))) { int localId = static_cast(response["id"].toDouble()); switch (m_requests[localId]) { case ListQueues: emit queueListReceived(response["result"].toObject()); break; case SubmitJob: emit submitJobResponse(localId, static_cast(response["result"] .toObject()["moleQueueId"].toDouble())); break; case LookupJob: emit lookupJobResponse(localId, response["result"].toObject()); break; case CancelJob: emit cancelJobResponse(static_cast(response["result"] .toObject()["moleQueueId"].toDouble())); break; case RegisterOpenWith: emit registerOpenWithResponse(localId); break; case ListOpenWithNames: emit listOpenWithNamesResponse(localId, response["result"].toArray()); break; case UnregisterOpenWith: emit unregisterOpenWithResponse(localId); break; default: break; } } } void Client::processNotification(const QJsonObject ¬ification) { if (notification["method"].toString() == "jobStateChanged") { QJsonObject params = notification["params"].toObject(); emit jobStateChanged( static_cast(params["moleQueueId"].toDouble()), params["oldState"].toString(), params["newState"].toString()); } } void Client::processError(const QJsonObject &error) { int localId = static_cast(error["id"].toDouble()); int errorCode = -1; QString errorMessage = tr("No message specified."); QJsonValue errorData; const QJsonValue &errorValue = error.value(QLatin1String("error")); if (errorValue.isObject()) { const QJsonObject errorObject = errorValue.toObject(); if (errorObject.value("code").isDouble()) errorCode = static_cast(errorObject.value("code").toDouble()); if (errorObject.value("message").isString()) errorMessage = errorObject.value("message").toString(); if (errorObject.contains("data")) errorData = errorObject.value("data"); } emit errorReceived(localId, errorCode, errorMessage, errorData); } QJsonObject Client::buildRegisterOpenWithRequest( const QString &name, const QList &filePatterns, const QJsonObject &handlerMethod) { QJsonArray patterns; foreach (const QRegExp ®ex, filePatterns) { QJsonObject pattern; switch (regex.patternSyntax()) { case QRegExp::RegExp: case QRegExp::RegExp2: pattern["regexp"] = regex.pattern(); break; case QRegExp::Wildcard: case QRegExp::WildcardUnix: pattern["wildcard"] = regex.pattern(); break; default: case QRegExp::FixedString: case QRegExp::W3CXmlSchema11: continue; } pattern["caseSensitive"] = regex.caseSensitivity() == Qt::CaseSensitive; patterns.append(pattern); } QJsonObject params; params["name"] = name; params["method"] = handlerMethod; params["patterns"] = patterns; QJsonObject packet = m_jsonRpcClient->emptyRequest(); packet["method"] = QLatin1String("registerOpenWith"); packet["params"] = params; return packet; } } // End namespace MoleQueue molequeue-0.9.0/molequeue/client/client.h000066400000000000000000000201551323436134600204370ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012-2013 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_CLIENT_H #define MOLEQUEUE_CLIENT_H #include "molequeueclientexport.h" #include #include #include #include #include namespace MoleQueue { class JsonRpcClient; class JobObject; /** * @class Client client.h * @brief The Client class is used by clients to submit jobs to a running * MoleQueue server. * @author Marcus D. Hanwell * * Provides a simple Qt C++ API to use the MoleQueue JSON-RPC calls to submit * and query the state of submitted jobs. */ class MOLEQUEUECLIENT_EXPORT Client : public QObject { Q_OBJECT public: explicit Client(QObject *parent_ = 0); ~Client(); /** * Query if the client is connected to a server. * @return True if connected, false if not. */ bool isConnected() const; public slots: /** * Connect to the server. * @param serverName Name of the socket to connect to, the default of * "MoleQueue" is usually correct when connecting to the running MoleQueue. */ bool connectToServer(const QString &serverName = "MoleQueue"); /** * Request the list of queues and programs from the server. The signal * queueListReceived() will be emitted once this has been received. * @return The local ID of the job submission request. */ int requestQueueList(); /** * Submit a job to MoleQueue. If the returned local ID is retained the signal * for a job submission will provide the MoleQueue ID along with the local ID. * @param job The job specification to be submitted to MoleQueue. * @return The local ID of the job submission request. */ int submitJob(const JobObject &job); /** * Request information about a job. You should supply the MoleQueue ID that * was received in response to a job submission. * @param moleQueueId The MoleQueue ID for the job. * @return The local ID of the job submission request. */ int lookupJob(unsigned int moleQueueId); /** * Cancel a job that was submitted. * @param moleQueueId The MoleQueue ID for the job. * @return The local ID of the job submission request. */ int cancelJob(unsigned int moleQueueId); /** * Register an executable file handler with MoleQueue. * @param name GUI name of the file handler. * @param executable Executable to call with the filename as the first * argument. If the full path to the exectuble is not specified, it must be * in the user's $PATH. * @param filePatterns A list of QRegExp objects that the handler can open. * The QRegExp objects must use RegExp, RegExp2, WildCard, or WildCardUnix * pattern syntax, else they will be ignored. * @return The local ID of the request. * @note The executable is expected to use the following calling convention * to open files: ~~~ executable /absolute/path/to/selected/fileName ~~~ */ int registerOpenWith(const QString &name, const QString &executable, const QList &filePatterns); /** * Register a JSON-RPC 2.0 local socket file handler with MoleQueue. * @param name GUI name of the file handler. * @param rpcServer Name of the local socket that the server is listening on. * @param rpcMethod JSON-RPC 2.0 request method to use. * @param filePatterns A list of QRegExp objects that the handler can open. * The QRegExp objects must use RegExp, RegExp2, WildCard, or WildCardUnix * pattern syntax, else they will be ignored. * @return The local ID of the request. * @note The following JSON-RPC 2.0 request is sent to the server when the * handler is activated: ~~~ { "jsonrpc": "2.0", "method": "", "params": { "fileName": "/absolute/path/to/selected/fileName" } }, "id": "XXX" } ~~~ * where is replaced by the @a rpcMethod argument. */ int registerOpenWith(const QString &name, const QString &rpcServer, const QString &rpcMethod, const QList &filePatterns); /** * @brief Request a list of all file handler names. * @return The local ID of the request. */ int listOpenWithNames(); /** * @brief Unregister the indicated file handler from the molequeue server. * @param handlerName Name of the file handler to remove. * @return The local ID of the request. * @sa listOpenWithNames */ int unregisterOpenWith(const QString &handlerName); /** * @brief flush Flush all pending messages to the server. * @warning This should not need to be called if used in an event loop, as Qt * will start writing to the socket as soon as control returns to the event * loop. */ void flush(); signals: /** * Emitted when the connection state changes. */ void connectionStateChanged(); /** * Emitted when the remote queue list is received. This gives a list of lists, * the primary key is the queue name, and that contains a list of available * programs for each queue. * @param queues A JSON object containing the names of the queues and the * programs each queue have available. */ void queueListReceived(QJsonObject queues); /** * Emitted when the job request response is received. * @param localId The local ID the job submission response is in reply to. * @param moleQueueId The remote MoleQueue ID for the job submission (can be * used to perform further actions on the job). */ void submitJobResponse(int localId, unsigned int moleQueueId); /** * Emitted when a job lookup response is received. * @param localId The local ID the job submission response is in reply to. * @param jobInfo A Json object containing all available job information. */ void lookupJobResponse(int localId, QJsonObject jobInfo); /** * Emitted when a job is successfully cancelled. */ void cancelJobResponse(unsigned int moleQueueId); /** * Emitted when the job state changes. */ void jobStateChanged(unsigned int moleQueueId, QString oldState, QString newState); /** * Emitted when a successful registerOpenWith response is received. */ void registerOpenWithResponse(int localId); /** * Emitted when a successful listOpenWithNames response is received. */ void listOpenWithNamesResponse(int localId, QJsonArray handlerNames); /** * Emitted when a successful unregisterOpenWith response is received. */ void unregisterOpenWithResponse(int localId); /** * Emitted when an error response is received. */ void errorReceived(int localId, int errorCode, QString errorMessage, QJsonValue errorData); protected slots: /** Parse the response object and emit the appropriate signal(s). */ void processResult(const QJsonObject &response); /** Parse a notification object and emit the appropriate signal(s). */ void processNotification(const QJsonObject ¬ification); /** Parse an error object and emit the appropriate signal(s). */ void processError(const QJsonObject ¬ification); protected: enum MessageType { Invalid = -1, ListQueues, SubmitJob, CancelJob, LookupJob, RegisterOpenWith, ListOpenWithNames, UnregisterOpenWith }; JsonRpcClient *m_jsonRpcClient; QHash m_requests; private: QJsonObject buildRegisterOpenWithRequest(const QString &name, const QList &filePatterns, const QJsonObject &handlerMethod); }; } // End namespace MoleQueue #endif // MOLEQUEUE_CLIENT_H molequeue-0.9.0/molequeue/client/jobobject.cpp000066400000000000000000000065011323436134600214540ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "jobobject.h" #include namespace MoleQueue { JobObject::JobObject() { } JobObject::~JobObject() { } void JobObject::setValue(const QString &key, const QVariant &value_) { m_value[key] = QJsonValue::fromVariant(value_); } QVariant JobObject::value(const QString &key, const QVariant &defaultValue) const { return m_value.contains(key) ? m_value[key].toVariant() : defaultValue; } void JobObject::setQueue(const QString &queueName) { m_value["queue"] = queueName; } QString JobObject::queue() const { return m_value["queue"].toString(); } void JobObject::setProgram(const QString &programName) { m_value["program"] = programName; } QString JobObject::program() const { return m_value["program"].toString(); } void JobObject::setDescription(const QString &descriptionText) { m_value["description"] = descriptionText; } QString JobObject::description() const { return m_value["description"].toString(); } void JobObject::setInputFile(const QString &fileName, const QString &contents) { m_value["inputFile"] = fileSpec(fileName, contents); } void JobObject::setInputFile(const QString &path) { m_value["inputFile"] = fileSpec(path); } void JobObject::setInputFile(const QJsonObject &file) { m_value["inputFile"] = file; } QJsonObject JobObject::inputFile() const { return m_value["inputFile"].toObject(); } void JobObject::appendAdditionalInputFile(const QString &fileName, const QString &contents) { QJsonArray extraInputFiles; if (m_value["additionalInputFiles"].isArray()) extraInputFiles = m_value["additionalInputFiles"].toArray(); extraInputFiles.append(fileSpec(fileName, contents)); m_value["additionalInputFiles"] = extraInputFiles; } void JobObject::appendAdditionalInputFile(const QString &path) { QJsonArray extraInputFiles; if (m_value["additionalInputFiles"].isArray()) extraInputFiles = m_value["additionalInputFiles"].toArray(); extraInputFiles.append(fileSpec(path)); m_value["additionalInputFiles"] = extraInputFiles; } void JobObject::setAdditionalInputFiles(const QJsonArray &files) { m_value["additionalInputFiles"] = files; } void JobObject::clearAdditionalInputFiles() { m_value.remove("additionalInputFiles"); } QJsonArray JobObject::additionalInputFiles() const { return m_value["additionalInputFiles"].toArray(); } QJsonObject JobObject::fileSpec(const QString &fileName, const QString &contents) { QJsonObject result; result["filename"] = fileName; result["contents"] = contents; return result; } QJsonObject JobObject::fileSpec(const QString &path) { QJsonObject result; result["path"] = path; return result; } } // End namespace MoleQueue molequeue-0.9.0/molequeue/client/jobobject.h000066400000000000000000000124401323436134600211200ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_JOBOBJECT_H #define MOLEQUEUE_JOBOBJECT_H #include "molequeueclientexport.h" #include #include #include namespace MoleQueue { /** * @class JobObject jobobject.h * @brief Simple client-side representation for a MoleQueue job. * @author Marcus D. Hanwell * * The Job class provides a simple interface to the client side representation * of a job to be submitted to MoleQueue. Any fields that are not set/present * will be omitted entirely, or set to default values by MoleQueue. The internal * representation of a job (and the transport used) is JSON. * * The Job class and data structure is very lightweight, and designed to be * easily copied, modified and passed around. */ class MOLEQUEUECLIENT_EXPORT JobObject { public: JobObject(); ~JobObject(); /** Set the @p value of the specified @p key. */ void setValue(const QString &key, const QVariant &value); /** Get the value of the specified @p key. If the key is not set, return * @p defaultValue. */ QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const; /** * Set the job up using the supplied JSON object. This replaces all previous * settings that may have been applied. */ void fromJson(const QJsonObject &jsonObject) { m_value = jsonObject; } /** Get the JSON object with the current job settings in it. */ QJsonObject json() const { return m_value; } /** * Set the queue that the job should be submitted to. This must be a valid * queue name discovered using the client API. */ void setQueue(const QString &queueName); /** * Get the name of the queue that the job will be submitted to. An empty * string means that no queue has been set. */ QString queue() const; /** * Set the program that the job should be submitted to. This must be a valid * program in a valid queue as discovered using the client API. */ void setProgram(const QString &programName); /** * Get the name of the program that the job will be submitted to. An empty * string means that no program has been set. */ QString program() const; /** * Set the description of the job, this is free text. */ void setDescription(const QString &descriptionText); /** * Get the description of the job. */ QString description() const; /** * @brief Set the input file for the job. * @param fileName The file name as it will appear in the working directory. * @param contents The contents of the file specified. */ void setInputFile(const QString &fileName, const QString &contents); /** * Set the input file for the job, the file will be copied and the file name * used in the working directory of the job submission. * \param path The full path to the input file. */ void setInputFile(const QString &path); /** * Set the input file using a JSON object. This must conform to the file * specification. * @param file A JSON object employing file specification to specify input. */ void setInputFile(const QJsonObject &file); /** * Get the input file for the job. This is a JSON object using the file spec. */ QJsonObject inputFile() const; /** * Append an additional input file for the job. * @param fileName The file name as it will appear in the working directory. * @param contents The contents of the file specified. */ void appendAdditionalInputFile(const QString &fileName, const QString &contents); /** * Append an additional input file for the job, the file will be copied and * the file name used in the working directory of the job submission. * @param path The full path to the input file. */ void appendAdditionalInputFile(const QString &path); /** * Set the additional input file using a JSON object. This must conform to the * file specification. * @param files A JSON array employing file specification to specify input. */ void setAdditionalInputFiles(const QJsonArray &files); /** Clear additional input files. */ void clearAdditionalInputFiles(); /** * Get the additional input files for the job. This is a JSON object using the * file spec. */ QJsonArray additionalInputFiles() const; protected: QJsonObject m_value; /** * Generate a filespec JSON object form the supplied file name and contents. */ QJsonObject fileSpec(const QString &fileName, const QString &contents); /** * Generate a filespec JSON object form the supplied file path (must exist). */ QJsonObject fileSpec(const QString &path); }; } // End namespace MoleQueue #endif // MOLEQUEUE_JOBOBJECT_H molequeue-0.9.0/molequeue/client/jsonrpcclient.cpp000066400000000000000000000073451323436134600223770ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012-2013 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "jsonrpcclient.h" #include #include #include #include namespace MoleQueue { JsonRpcClient::JsonRpcClient(QObject *parent_) : QObject(parent_), m_packetCounter(0), m_socket(NULL) { connect(this, SIGNAL(newPacket(QByteArray)), SLOT(readPacket(QByteArray)), Qt::QueuedConnection); } JsonRpcClient::~JsonRpcClient() { flush(); } bool JsonRpcClient::isConnected() const { if (!m_socket) return false; else return m_socket->isOpen(); } bool JsonRpcClient::connectToServer(const QString &serverName_) { if (m_socket && m_socket->isOpen()) { if (m_socket->serverName() == serverName_) { return false; } else { m_socket->close(); delete m_socket; m_socket = NULL; } } // New connection. if (m_socket == NULL) { m_socket = new QLocalSocket(this); connect(m_socket, SIGNAL(readyRead()), this, SLOT(readSocket())); } if (serverName_.isEmpty()) { return false; } else { m_socket->connectToServer(serverName_); return isConnected(); } } QString JsonRpcClient::serverName() const { if (m_socket) return m_socket->serverName(); else return QString(); } void JsonRpcClient::flush() { if (m_socket) m_socket->flush(); } QJsonObject JsonRpcClient::emptyRequest() { QJsonObject request; request["jsonrpc"] = QLatin1String("2.0"); request["id"] = static_cast(m_packetCounter++); return request; } bool JsonRpcClient::sendRequest(const QJsonObject &request) { if (!m_socket) return false; QJsonDocument document(request); QDataStream stream(m_socket); stream.setVersion(QDataStream::Qt_4_8); stream << document.toJson(); return true; } void JsonRpcClient::readPacket(const QByteArray message) { // Read packet into a Json value QJsonParseError error; QJsonDocument reader = QJsonDocument::fromJson(message, &error); if (error.error != QJsonParseError::NoError) { emit badPacketReceived("Unparseable message received\n:" + error.errorString() + "\nContent: " + message); return; } else if (!reader.isObject()) { // We need a valid object, something bad happened. emit badPacketReceived("Packet did not contain a valid JSON object."); return; } else { QJsonObject root = reader.object(); if (root["method"] != QJsonValue::Null) { if (root["id"] != QJsonValue::Null) emit badPacketReceived("Received a request packet for the client."); else emit notificationReceived(root); } if (root["result"] != QJsonValue::Null) { // This is a result packet, and should emit a signal. emit resultReceived(root); } else if (root["error"] != QJsonValue::Null) { emit errorReceived(root); } } } void JsonRpcClient::readSocket() { if (m_socket->bytesAvailable() > 0) { QDataStream stream(m_socket); QByteArray json; stream >> json; emit newPacket(json); if (m_socket->bytesAvailable() > 0) QTimer::singleShot(0, this, SLOT(readSocket())); } } } // End namespace MoleQueue molequeue-0.9.0/molequeue/client/jsonrpcclient.h000066400000000000000000000100361323436134600220330ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012-2013 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_JSONRPCCLIENT_H #define MOLEQUEUE_JSONRPCCLIENT_H #include "molequeueclientexport.h" #include #include class QLocalSocket; namespace MoleQueue { /** * @class JsonRpcClient jsonrpcclient.h * @brief The JsonRpcClient class is used by clients to submit calls to an RPC * server using JSON-RPC 2.0. * @author Marcus D. Hanwell * * Provides a simple Qt C++ API to make JSON-RPC 2.0 calls to an RPC server. To * create a client connection and call a method the following should be done: * @code #include MoleQueue::JsonRpcClient *client = new MoleQueue::JsonRpcClient(this); client->connectToServer("MyRpcServer"); QJsonObject request(client->emptyRequest()); request["method"] = QLatin1String("listQueues"); client->sendRequest(request); @endcode * * You should connect to the appropriate signals in order to act on results, * notifications and errors received in response to requests set using the * client connection. */ class MOLEQUEUECLIENT_EXPORT JsonRpcClient : public QObject { Q_OBJECT public: explicit JsonRpcClient(QObject *parent_ = 0); ~JsonRpcClient(); /** * Query if the client is connected to a server. * @return True if connected, false if not. */ bool isConnected() const; /** * @return The server name that the client is connected to. */ QString serverName() const; public slots: /** * Connect to the server. * @param serverName Name of the socket to connect to. */ bool connectToServer(const QString &serverName); /** * @brief flush Flush all pending messages to the server. * @warning This should not need to be called if used in an event loop, as Qt * will start writing to the socket as soon as control returns to the event * loop. */ void flush(); /** * Use this function to construct an empty JSON-RPC 2.0 request, with a valid * request id, JSON-RPC 2.0 key etc. * @return a standard empty JSON-RPC 2.0 packet, the method etc is empty. */ QJsonObject emptyRequest(); /** * Send the Json request to the RPC server. * @param request The JSON-RPC 2.0 request object. * @return True on success, false on failure. */ bool sendRequest(const QJsonObject &request); protected slots: /** * Read incoming packets of data from the server. */ void readPacket(const QByteArray message); /** * Read incoming data, interpret JSON stream. */ void readSocket(); signals: /** * Emitted when the connection state changes. */ void connectionStateChanged(); /** * Emitted when a result is received. */ void resultReceived(QJsonObject message); /** * Emitted when a notification is received. */ void notificationReceived(QJsonObject message); /** * Emitted when an error response is received. */ void errorReceived(QJsonObject message); /** * Emitted when a bad packet was received that the client could not parse. */ void badPacketReceived(QString error); /** * Emitted when a new packet of data is received. This is handled internally, * other classes should listen to resultReceived, notificationReceived, * errorReceived, and badPacketReceived. */ void newPacket(const QByteArray &packet); protected: unsigned int m_packetCounter; QLocalSocket *m_socket; }; } // End namespace MoleQueue #endif // MOLEQUEUE_JSONRPCCLIENT_H molequeue-0.9.0/molequeue/lastinstall/000077500000000000000000000000001323436134600200615ustar00rootroot00000000000000molequeue-0.9.0/molequeue/lastinstall/CMakeLists.txt000066400000000000000000000021021323436134600226140ustar00rootroot00000000000000if((APPLE OR WIN32) AND NOT ${CMAKE_VERSION} VERSION_LESS 2.8.8) find_package(Qt5 COMPONENTS Core REQUIRED) set(pfx "") if(NOT APPLE) set(pfx "bin/") endif() set(sfx "") if(APPLE) set(sfx ".app") elseif(WIN32) set(sfx ".exe") endif() get_target_property(output_name molequeue OUTPUT_NAME) if(output_name) set(exe "${pfx}${output_name}${sfx}") else() set(exe "${pfx}molequeue${sfx}") endif() get_property(MoleQueue_PLUGINS GLOBAL PROPERTY MoleQueue_PLUGINS) set(plugins "") foreach(plugin ${MoleQueue_PLUGINS}) # get_property(location TARGET ${plugin} PROPERTY LOCATION) list(APPEND plugins ${plugin}) endforeach() set(dirs "${CMAKE_INSTALL_PREFIX}/${INSTALL_LIBRARY_DIR}") if(CMAKE_PREFIX_PATH) foreach(dir ${CMAKE_PREFIX_PATH}) list(APPEND dirs "${dir}/bin" "${dir}/lib") endforeach() endif() find_package(Qt5 COMPONENTS Widgets Network REQUIRED) include(InstallRequiredSystemLibraries) include(DeployQt5) install_qt5_executable(${exe} "${plugins}" "" "${dirs}" "lib/molequeue/plugins") endif() molequeue-0.9.0/molequeue/plugins/000077500000000000000000000000001323436134600172105ustar00rootroot00000000000000molequeue-0.9.0/molequeue/plugins/CMakeLists.txt000066400000000000000000000005511323436134600217510ustar00rootroot00000000000000find_package(Qt5 COMPONENTS Network REQUIRED) if (MSVC) # Do not generate manifests for the plugins - caused issues loading plugins set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /MANIFEST:NO") endif() set_property(GLOBAL PROPERTY MoleQueue_PLUGINS) add_subdirectory(localsocketserver) if(USE_ZERO_MQ) add_subdirectory(zeromqserver) endif() molequeue-0.9.0/molequeue/plugins/localsocketserver/000077500000000000000000000000001323436134600227425ustar00rootroot00000000000000molequeue-0.9.0/molequeue/plugins/localsocketserver/CMakeLists.txt000066400000000000000000000010111323436134600254730ustar00rootroot00000000000000set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/../lib/molequeue/plugins) add_library(LocalSocketServer MODULE localsocketconnectionlistenerfactory.cpp) qt5_use_modules(LocalSocketServer Network) set_target_properties(LocalSocketServer PROPERTIES AUTOMOC TRUE PREFIX "") target_link_libraries(LocalSocketServer MoleQueueServerCore) set_property(GLOBAL APPEND PROPERTY MoleQueue_PLUGINS LocalSocketServer) install(TARGETS LocalSocketServer DESTINATION ${INSTALL_LIBRARY_DIR}/molequeue/plugins) molequeue-0.9.0/molequeue/plugins/localsocketserver/localsocketconnectionlistenerfactory.cpp000066400000000000000000000023511323436134600331700ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "localsocketconnectionlistenerfactory.h" #include namespace MoleQueue { LocalSocketConnectionListenerFactory::LocalSocketConnectionListenerFactory() { } LocalSocketConnectionListenerFactory::~LocalSocketConnectionListenerFactory() { } ConnectionListener * LocalSocketConnectionListenerFactory::createConnectionListener(QObject *parentObject, const QString &connectionString) { return new LocalSocketConnectionListener(parentObject, connectionString); } } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/plugins/localsocketserver/localsocketconnectionlistenerfactory.h000066400000000000000000000030171323436134600326350ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_LOCALSOCKETCONNECTIONLISTENERFACTORY_H #define MOLEQUEUE_LOCALSOCKETCONNECTIONLISTENERFACTORY_H #include namespace MoleQueue { /// @brief Subclass to ConnectionListenerFactory which uses local sockets. class LocalSocketConnectionListenerFactory: public QObject, public MoleQueue::ConnectionListenerFactory { Q_OBJECT Q_PLUGIN_METADATA(IID "org.openchemistry.molequeue.ConnectionListenerFactory") Q_INTERFACES(MoleQueue::ConnectionListenerFactory) public: LocalSocketConnectionListenerFactory(); ~LocalSocketConnectionListenerFactory(); ConnectionListener *createConnectionListener(QObject *parentObject, const QString &connectionString = "MoleQueue"); }; } // namespace MoleQueue #endif // MOLEQUEUE_LOCALSOCKETCONNECTIONLISTENERFACTORY_H molequeue-0.9.0/molequeue/plugins/zeromqserver/000077500000000000000000000000001323436134600217545ustar00rootroot00000000000000molequeue-0.9.0/molequeue/plugins/zeromqserver/CMakeLists.txt000066400000000000000000000010151323436134600245110ustar00rootroot00000000000000set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/../lib/molequeue/plugins) find_package(ZeroMQ REQUIRED) include_directories(SYSTEM ${ZeroMQ_INCLUDE_DIR}) add_library(ZeroMqServer MODULE zeromqconnectionlistenerfactory.cpp) set_target_properties(ZeroMqServer PROPERTIES AUTOMOC TRUE PREFIX "") target_link_libraries(ZeroMqServer MoleQueueZeroMq) set_property(GLOBAL APPEND PROPERTY MoleQueue_PLUGINS ZeroMqServer) install(TARGETS ZeroMqServer DESTINATION ${INSTALL_LIBRARY_DIR}/molequeue/plugins) molequeue-0.9.0/molequeue/plugins/zeromqserver/zeromqconnectionlistenerfactory.cpp000066400000000000000000000025001323436134600312100ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "zeromqconnectionlistenerfactory.h" #include #include namespace MoleQueue { ZeroMqConnectionListenerFactory::ZeroMqConnectionListenerFactory() { } ConnectionListener *ZeroMqConnectionListenerFactory::createConnectionListener(QObject *parentObject, const QString &connectionString) { QString connectionPath = QDir::temp().path() + "/" + ZeroMqConnection::zeroMqPrefix + "_" + connectionString; return new ZeroMqConnectionListener(parentObject, "ipc://" + connectionPath); } } // namespace MoleQueue molequeue-0.9.0/molequeue/plugins/zeromqserver/zeromqconnectionlistenerfactory.h000066400000000000000000000026741323436134600306710ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_ZEROMQCONNECTIONLISTENERFACTORY_H #define MOLEQUEUE_ZEROMQCONNECTIONLISTENERFACTORY_H #include namespace MoleQueue { /// @brief A ConnectionListenerFactory subclass using ZeroMQ. class ZeroMqConnectionListenerFactory: public QObject, public MoleQueue::ConnectionListenerFactory { Q_OBJECT Q_PLUGIN_METADATA(IID "org.openchemistry.molequeue.ConnectionListenerFactory") Q_INTERFACES(MoleQueue::ConnectionListenerFactory) public: ZeroMqConnectionListenerFactory(); ConnectionListener *createConnectionListener(QObject *parentObject = 0, const QString &connectionString = "MoleQueue"); }; } // namespace MoleQueue #endif // MOLEQUEUE_ZEROMQCONNECTIONLISTENERFACTORY_H molequeue-0.9.0/molequeue/servercore/000077500000000000000000000000001323436134600177065ustar00rootroot00000000000000molequeue-0.9.0/molequeue/servercore/CMakeLists.txt000066400000000000000000000022721323436134600224510ustar00rootroot00000000000000find_package(Qt5 COMPONENTS Network REQUIRED) include(GenerateExportHeader) add_library(MoleQueueServerCore connection.h connectionlistener.h jsonrpc.cpp localsocketconnection.cpp localsocketconnectionlistener.cpp message.cpp messageidmanager_p.cpp ) qt5_use_modules(MoleQueueServerCore Network) set_target_properties(MoleQueueServerCore PROPERTIES AUTOMOC TRUE) #target_link_libraries(MoleQueueServerCore) set(hdrs connection.h connectionlistener.h connectionlistenerfactory.h jsonrpc.h localsocketconnection.h localsocketconnectionlistener.h message.h servercoreglobal.h ) generate_export_header(MoleQueueServerCore EXPORT_FILE_NAME molequeueservercoreexport.h) include_directories(${CMAKE_CURRENT_BINARY_DIR}) set_property(TARGET MoleQueueServerCore APPEND PROPERTY COMPILE_FLAGS ${molequeue_export_flags}) list(APPEND hdrs "${CMAKE_CURRENT_BINARY_DIR}/molequeueservercoreexport.h") install(FILES ${hdrs} DESTINATION "${INSTALL_INCLUDE_DIR}/molequeue/servercore") install(TARGETS MoleQueueServerCore EXPORT "MoleQueueTargets" RUNTIME DESTINATION ${INSTALL_RUNTIME_DIR} LIBRARY DESTINATION ${INSTALL_LIBRARY_DIR} ARCHIVE DESTINATION ${INSTALL_ARCHIVE_DIR}) molequeue-0.9.0/molequeue/servercore/connection.h000066400000000000000000000050371323436134600222230ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_CONNECTION_H #define MOLEQUEUE_CONNECTION_H #include #include "molequeueservercoreexport.h" #include "servercoreglobal.h" namespace MoleQueue { /** * @class Connection connection.h * @brief The Connection class is an interface defining the connection using to * communicate between MoleQueue processes. Subclasses provide concrete * implements for example based on local socket @see LocalSocketConnection */ class MOLEQUEUESERVERCORE_EXPORT Connection : public QObject { Q_OBJECT public: /** * Constructor. * * @param parentObject parent */ explicit Connection(QObject *parentObject = 0 ) : QObject(parentObject) {} /** * Open the connection */ virtual void open() = 0; /** * Start receiving messages on this connection */ virtual void start() = 0; /** * Close the connection. Once a conneciton is closed if can't reused. */ virtual void close() = 0; /* * @return true, if the connection is open ( open has been called, * false otherwise */ virtual bool isOpen() = 0; /** * @return the connect string description the endpoint the connection is * connected to. */ virtual QString connectionString() const = 0; /** * Send the @a packet on the connection to @a endpoint. */ virtual bool send(const PacketType &packet, const EndpointIdType &endpoint) = 0; /** * Flush all pending messages to the other endpoint. */ virtual void flush() = 0; signals: /** * Emitted when a new message has been received on this connection. * * @param msg The message received. */ void packetReceived(const MoleQueue::PacketType &packet, const MoleQueue::EndpointIdType &endpoint); /** * Emited when the connection is disconnected. */ void disconnected(); }; } // end namespace MoleQueue #endif // MOLEQUEUE_CONNECTION_H molequeue-0.9.0/molequeue/servercore/connectionlistener.h000066400000000000000000000054621323436134600237730ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_CONNECTIONLISTENER_H #define MOLEQUEUE_CONNECTIONLISTENER_H #include #include #include "connection.h" namespace MoleQueue { /** * @class ConnectionListener connectionlistener.h * * @brief The ConnectionListener class is an interface defining a listener waiting * for connection to a server. Implementations should emit the @newConnection() * signal. Subclasses provide concrete implements for example based on local sockets * @see LocalSocketConnectionListener */ class MOLEQUEUESERVERCORE_EXPORT ConnectionListener : public QObject { Q_OBJECT Q_ENUMS(Error) public: /** * Constructor. * * @param parentObject parent */ ConnectionListener(QObject *parentObject = 0 ) : QObject(parentObject) {} /** * Start the connection listener, start listening for incoming connections. */ virtual void start() = 0; /** * Stop the connection listener. * * @param force if true, "extreme" measures may be taken to stop the listener. */ virtual void stop(bool force) = 0; /** * Stop the connection listener without forcing it, equivalent to stop(false) * * @see stop(bool) */ virtual void stop() = 0; /** * @return the "address" the listener will listen on. */ virtual QString connectionString() const = 0; /** * Defines the errors that will be emitted by @connectionError() */ enum Error { AddressInUseError, UnknownError = -1 }; signals: /** * Emitted when a new connection is received. The new connection is only * valid for the lifetime of the connection listener instance that emitted * it. * * @param The new connection. */ void newConnection(MoleQueue::Connection *connection); /** * Emitted when an error occurs. * * @param errorCore The error code @see Error * @param message The error message provided by the implementation. */ void connectionError(MoleQueue::ConnectionListener::Error errorCode, const QString &message); }; } // end namespace MoleQueue Q_DECLARE_METATYPE(MoleQueue::ConnectionListener::Error) #endif // MOLEQUEUE_CONNECTIONLISTENER_H molequeue-0.9.0/molequeue/servercore/connectionlistenerfactory.h000066400000000000000000000025321323436134600253560ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_CONNECTIONLISTENERFACTORY_H #define MOLEQUEUE_CONNECTIONLISTENERFACTORY_H #include class QObject; class QString; namespace MoleQueue { class ConnectionListener; /// @brief Factory for generating ConnectionListener instances. class ConnectionListenerFactory { public: virtual ~ConnectionListenerFactory() {} virtual ConnectionListener *createConnectionListener(QObject *parentObject, const QString &connectionString) = 0; }; } // namespace MoleQueue Q_DECLARE_INTERFACE(MoleQueue::ConnectionListenerFactory, "org.openchemistry.molequeue.ConnectionListenerFactory") #endif // MOLEQUEUE_CONNECTIONLISTENERFACTORY_H molequeue-0.9.0/molequeue/servercore/jsonrpc.cpp000066400000000000000000000150121323436134600220670ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "jsonrpc.h" #include "connectionlistener.h" #include #include #include namespace MoleQueue { JsonRpc::JsonRpc(QObject *parent_) : QObject(parent_) { qRegisterMetaType("MoleQueue::Message"); qRegisterMetaType("MoleQueue::PacketType"); qRegisterMetaType("MoleQueue::EndpointIdType"); } JsonRpc::~JsonRpc() { } void JsonRpc::addConnectionListener(ConnectionListener *connlist) { if (m_connections.keys().contains((connlist))) return; m_connections.insert(connlist, QList()); connect(connlist, SIGNAL(newConnection(MoleQueue::Connection*)), SLOT(addConnection(MoleQueue::Connection*))); connect(connlist, SIGNAL(destroyed()), SLOT(removeConnectionListenerInternal())); } void JsonRpc::removeConnectionListener(ConnectionListener *connlist) { disconnect(0, connlist); foreach(Connection *conn, m_connections.value(connlist)) this->removeConnection(connlist, conn); m_connections.remove(connlist); } void JsonRpc::addConnection(Connection *conn) { ConnectionListener *connlist = qobject_cast(sender()); if (!connlist || !m_connections.keys().contains(connlist)) return; QList &conns = m_connections[connlist]; if (conns.contains(conn)) return; conns << conn; connect(conn, SIGNAL(destroyed()), SLOT(removeConnection())); connect(conn, SIGNAL(packetReceived(MoleQueue::PacketType, MoleQueue::EndpointIdType)), SLOT(newPacket(MoleQueue::PacketType,MoleQueue::EndpointIdType))); conn->start(); } void JsonRpc::removeConnection(ConnectionListener *connlist, Connection *conn) { disconnect(0, conn); if (!m_connections.contains(connlist)) return; QList &conns = m_connections[connlist]; conns.removeOne(conn); } void JsonRpc::removeConnection(Connection *conn) { // Find the connection listener: foreach (ConnectionListener *connlist, m_connections.keys()) { if (m_connections[connlist].contains(conn)) { removeConnection(connlist, conn); return; } } } void JsonRpc::removeConnection() { // Use a reinterpret cast -- this is connected to a QObject::destroyed() // signal, and we can't qobject_cast to a Connection* after the Connection // destructor has run. Since this is a private slot, we can ensure that only // Connections will be connected to it. This pointer is never dereferenced // and is ignored if it can't be found in m_connections, so there are no ill // effects if sender() is not a Connection (other than a few wasted cycles). if (Connection *conn = reinterpret_cast(sender())) removeConnection(conn); } void JsonRpc::removeConnectionListenerInternal() { // Use a reinterpret cast -- this is connected to a QObject::destroyed() // signal, and we can't qobject_cast to a ConnectionListener* after the // ConnectionListener destructor has run. Since this is a private slot, we can // ensure that only ConnectionListeners will be connected to it. This pointer // is never dereferenced and is ignored if it can't be found in m_connections, // so there are no ill effects if sender() is not a ConnectionListener // (other than a few wasted cycles). if (ConnectionListener *cl = reinterpret_cast(sender())) removeConnectionListener(cl); } void JsonRpc::newPacket(const MoleQueue::PacketType &packet, const MoleQueue::EndpointIdType &endpoint) { Connection *conn = qobject_cast(sender()); if (!conn) return; // Parse the packet as JSON QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(QByteArray(packet), &error); // Send a server error and return if there was an issue parsing the packet. if (error.error != QJsonParseError::NoError || doc.isNull()) { Message errorMessage(Message::Error, conn, endpoint); errorMessage.setErrorCode(-32700); errorMessage.setErrorMessage("Parse error"); QJsonObject errorDataObject; errorDataObject.insert("QJsonParseError::error", error.error); errorDataObject.insert("QJsonParseError::errorString", error.errorString()); errorDataObject.insert("QJsonParseError::offset", error.offset); errorDataObject.insert("bytes received", QLatin1String(packet.constData())); errorMessage.send(); return; } // Pass the JSON off for further processing. Must be an array or object. handleJsonValue(conn, endpoint, doc.isArray() ? QJsonValue(doc.array()) : QJsonValue(doc.object())); } void JsonRpc::handleJsonValue(Connection *conn, const EndpointIdType &endpoint, const QJsonValue &json) { // Handle batch requests recursively if (json.isArray()) { /// @todo Stage batch replies into an array before sending. foreach (const QJsonValue &val, json.toArray()) handleJsonValue(conn, endpoint, val); return; } // Objects are RPC calls if (!json.isObject()) { Message errorMessage(Message::Error, conn, endpoint); errorMessage.setErrorCode(-32600); errorMessage.setErrorMessage("Invalid Request"); QJsonObject errorDataObject; errorDataObject.insert("description", QLatin1String("Request is not a JSON " "object.")); errorDataObject.insert("request", json); errorMessage.send(); return; } Message message(json.toObject(), conn, endpoint); Message errorMessage; if (!message.parse(errorMessage)) { errorMessage.send(); return; } // Handle ping requests internally if (message.type() == Message::Request && message.method() == "internalPing") { Message response = message.generateResponse(); response.setResult(QLatin1String("pong")); response.send(); return; } emit messageReceived(message); } } // namespace MoleQueue molequeue-0.9.0/molequeue/servercore/jsonrpc.h000066400000000000000000000123561323436134600215440ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_JSONRPC_H #define MOLEQUEUE_JSONRPC_H #include "message.h" #include "servercoreglobal.h" #include #include #include class JsonRpcTest; namespace MoleQueue { class Connection; class ConnectionListener; /** * @class JsonRpc jsonrpc.h * @brief The JsonRpc class manages ConnectionListener and Connection instances, * and emits incoming JSON-RPC Messages. * @author David C. Lonie * * To use the JsonRpc class, create one or more ConnectionListener instances * and call addConnectionListener(). Connect a slot to messageReceived and * handle any incoming messages. * * This class will handle the following standard JSON-RPC errors: * * - -32600 Invalid request * - The message type could not be determined. * - -32603 Internal error * - Internal JSON-RPC error * - -32700 Parse error * - Invalid JSON received, an error occurred during parsing. * * The remaining standard JSON-RPC error codes should be handled by the * application developer when messageReceived: * * - -32601 Method not found * - Method not supported by application * - -32602 Invalid params * - Inappropriate parameters supplied for requested method. * * Incoming requests with method="internalPing" will be automatically replied * to with result="pong". This can be used to test if a server is alive or not. * messageReceived will not be emitted in this case. * * Use Message::generateResponse() and Message::generateErrorResponse() to * easily create replies to incoming requests. */ class MOLEQUEUESERVERCORE_EXPORT JsonRpc : public QObject { Q_OBJECT public: friend class ::JsonRpcTest; explicit JsonRpc(QObject *parent_ = 0); ~JsonRpc(); /** * @brief Register a connection listener with this * JsonRpc instance. Any incoming connections on the listener will be * monitored by this class and all incoming messages will be treated as * JSON-RPC transmissions. This class does not take ownership of the listener, * and will automatically remove it from any internal data structures if * it is destroyed. */ void addConnectionListener(MoleQueue::ConnectionListener *connlist); /** * @brief Unregister a connection listener from this * JsonRpc instance. Any connections owned by this listener will be * unregistered as well. * @param connlist */ void removeConnectionListener(MoleQueue::ConnectionListener *connlist); signals: /** * @brief Emitted when a valid message is received. */ void messageReceived(const MoleQueue::Message &message); private slots: /** * @brief Register a connection with this JsonRpc instance. * @note The sender must be the connection listener (connect to * MoleQueue::ConnectionListener::newConnection(MoleQueue::Connection*)) */ void addConnection(MoleQueue::Connection *conn); /** * @brief Unregister a connection with this JsonRpc instance. * @param connlist The connection listener which owns the connection. * @return True on success, false on failure (e.g. connection not found). */ void removeConnection(MoleQueue::ConnectionListener *connlist, MoleQueue::Connection *conn); /** * @brief Unregister a connection with this JsonRpc instance. * @return True on success, false on failure (e.g. connection not found). * @overload */ void removeConnection(MoleQueue::Connection *conn); /** * @brief Unregister a connection from this JsonRpc instance. * The sender that triggers this slot must be a subclass of * Connection, which will be removed. * @overload */ void removeConnection(); /** * @brief Unregister a connection listener * from this JsonRpc instance. The sender that triggers this slot must be a * subclass of Connection, which will be removed. * @overload */ void removeConnectionListenerInternal(); /** * @brief Called when a registered connection emits a new packet. * The packet is parsed into JSON and split if it is a batch request. Each * requested is parsed into a Message and messageReceived is emitted. * @note The sender must be the connection from which the packet originates. */ void newPacket(const MoleQueue::PacketType &packet, const MoleQueue::EndpointIdType &endpoint); private: /** * Helper function for newPacket. */ void handleJsonValue(Connection *conn, const EndpointIdType &endpoint, const QJsonValue &json); /// Container of all known connections and listeners. QMap > m_connections; }; } // namespace MoleQueue #endif // MOLEQUEUE_JSONRPC_H molequeue-0.9.0/molequeue/servercore/localsocketconnection.cpp000066400000000000000000000111641323436134600250000ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "localsocketconnection.h" #include #include #include #include namespace MoleQueue { LocalSocketConnection::LocalSocketConnection(QObject *parentObject, QLocalSocket *socket) : Connection(parentObject), m_connectionString(socket->serverName()), m_socket(NULL), m_dataStream(new QDataStream), m_holdRequests(true) { setSocket(socket); } LocalSocketConnection::LocalSocketConnection(QObject *parentObject, const QString &serverName) : Connection(parentObject), m_connectionString(serverName), m_socket(NULL), m_dataStream(new QDataStream), m_holdRequests(true) { setSocket(new QLocalSocket); } LocalSocketConnection::~LocalSocketConnection() { // Make sure we are closed close(); delete m_socket; m_socket = NULL; delete m_dataStream; m_dataStream = NULL; } void LocalSocketConnection::setSocket(QLocalSocket *socket) { if (m_socket != NULL) { m_socket->abort(); m_socket->disconnect(this); disconnect(m_socket); m_socket->deleteLater(); } if (socket != NULL) { connect(socket, SIGNAL(readyRead()), this, SLOT(readSocket())); connect(socket, SIGNAL(disconnected()), this, SIGNAL(disconnected())); connect(socket, SIGNAL(destroyed()), this, SLOT(socketDestroyed())); } m_dataStream->setDevice(socket); m_dataStream->setVersion(QDataStream::Qt_4_8); m_socket = socket; } void LocalSocketConnection::readSocket() { if(!m_socket->isValid()) return; if (m_holdRequests) return; if (m_socket->bytesAvailable() == 0) return; PacketType packet; (*m_dataStream) >> packet; emit packetReceived(packet, EndpointIdType()); // Check again in 50 ms if no more data is available, or immediately if there // is. This helps ensure that burst traffic is handled robustly. QTimer::singleShot(m_socket->bytesAvailable() > 0 ? 0 : 50, this, SLOT(readSocket())); } void LocalSocketConnection::open() { if (m_socket) { if(isOpen()) { qWarning() << "Socket already connected to" << m_connectionString; return; } m_socket->connectToServer(m_connectionString); } else { qWarning() << "No socket set, connection not opened."; } } void LocalSocketConnection::start() { if (m_socket) { m_holdRequests = false; while (m_socket->bytesAvailable() != 0) readSocket(); } } void LocalSocketConnection::close() { if(m_socket) { if(m_socket->isOpen()) { m_socket->disconnectFromServer(); m_socket->close(); } } } bool LocalSocketConnection::isOpen() { return m_socket != NULL && m_socket->isOpen(); } QString LocalSocketConnection::connectionString() const { return m_connectionString; } bool LocalSocketConnection::send(const PacketType &packet, const EndpointIdType &endpoint) { Q_UNUSED(endpoint); // Because of a possible bug with Qt 5.8 and 5.9 on Windows, // (*m_dataStream) << packet // sends two packets instead of one. The packets will fail to get read // correctly on the other side of the message. To fix this, we write the // message to a byte array and send it in all together as a single raw data // packet. If this bug gets fixed in the future, we will not need the // Windows section... // See https://bugreports.qt.io/browse/QTBUG-61097 for the bug report. #ifdef _WIN32 PacketType byteArray; QDataStream tmpStream(&byteArray, QIODevice::WriteOnly); tmpStream << packet; m_dataStream->writeRawData(byteArray, byteArray.size()); #else (*m_dataStream) << packet; #endif return true; } void LocalSocketConnection::flush() { m_socket->flush(); } void LocalSocketConnection::socketDestroyed() { // Set to NULL so we know we don't need to clean up m_socket = NULL; // Tell anyone listening we have been disconnected. emit disconnected(); } } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/servercore/localsocketconnection.h000066400000000000000000000070351323436134600244470ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_LOCALSOCKETCONNECTION_H #define MOLEQUEUE_LOCALSOCKETCONNECTION_H #include "connection.h" class QLocalSocket; namespace MoleQueue { /** * @class LocalSocketConnection localsocketconnection.h * * @brief Provides am implementation of @Connection using QLocalSockets. Each * instance of the class wraps a QLocalSocket. */ class MOLEQUEUESERVERCORE_EXPORT LocalSocketConnection : public Connection { Q_OBJECT public: /** * Constructor used by @LocalSocketConnectionListener to create a new connection * based on an existing QLocalSocket. * * @param parentObject parent * @param socket The socket that this connection instance will operate on. */ explicit LocalSocketConnection(QObject *parentObject, QLocalSocket *socket); /** * Constructor used by a client to connection to a server ( @ConnectionListener ) * * @param parentObject parent * @param connectionString The "address" of server to connect to. */ explicit LocalSocketConnection(QObject *parentObject, const QString &connectionString); /** * Destructor. */ ~LocalSocketConnection(); /** * Opens the connection the server i.e. QLocalSocket::connectToServer(...) * * @see Connection::open() */ void open(); /** * Start receiving messages on this connection. * * @see Connection::start */ void start(); /** * Close the underlying socket. Once closed the connection can no longer be used * to receive or send messages. * * @see Connection::close() */ void close(); /** * @return true is connection is open, false otherwise. * * @see Connection::isOpen() */ bool isOpen(); /** * @return The serverName from the underlying socket. * * @see Connection::connectionString() */ QString connectionString() const; bool send(const PacketType &packet, const EndpointIdType &endpoint); void flush(); private slots: /** * Read data from the local socket until a complete packet has been obtained. */ void readSocket(); /** * Called when the underlying QLocalSocket is destroyed. This happens when * the connection listener associated with it is deleted. */ void socketDestroyed(); private: /** * Sets the underlying local socket for this connection. * * @param socket The local socket to use on this connection. */ void setSocket(QLocalSocket *socket); /// The address the socket is connected to. QString m_connectionString; /// The underlying local socket QLocalSocket *m_socket; /// The data stream used to interface with the local socket QDataStream *m_dataStream; /// If true, do not read incoming packets from the socket. This is to let /// the parent server create connections prior to processing requests. bool m_holdRequests; }; } // namespace MoleQueue #endif // MOLEQUEUE_LOCALSOCKETCONNECTION_H molequeue-0.9.0/molequeue/servercore/localsocketconnectionlistener.cpp000066400000000000000000000052431323436134600265470ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "localsocketconnectionlistener.h" #include "localsocketconnection.h" #include #include namespace MoleQueue { LocalSocketConnectionListener::LocalSocketConnectionListener(QObject *parentObject, const QString &connString) : ConnectionListener(parentObject), m_connectionString(connString), m_server(new QLocalServer ()) { connect(m_server, SIGNAL(newConnection()), this, SLOT(newConnectionAvailable())); } LocalSocketConnectionListener::~LocalSocketConnectionListener() { // Make sure we are stopped stop(); delete m_server; m_server = NULL; } void LocalSocketConnectionListener::start() { if (!m_server->listen(m_connectionString)) { emit connectionError(toConnectionListenerError(m_server->serverError()), m_server->errorString()); return; } } void LocalSocketConnectionListener::stop(bool force) { if (force) QLocalServer::removeServer(m_connectionString); if (m_server) m_server->close(); } void LocalSocketConnectionListener::stop() { stop(false); } QString LocalSocketConnectionListener::connectionString() const { return m_connectionString; } QString LocalSocketConnectionListener::fullConnectionString() const { return m_server->fullServerName(); } void LocalSocketConnectionListener::newConnectionAvailable() { if (!m_server->hasPendingConnections()) return; QLocalSocket *socket = m_server->nextPendingConnection(); LocalSocketConnection *conn = new LocalSocketConnection(this, socket); emit newConnection(conn); } ConnectionListener::Error LocalSocketConnectionListener::toConnectionListenerError( QAbstractSocket::SocketError socketError) { ConnectionListener::Error listenerError = UnknownError; switch (socketError) { case QAbstractSocket::AddressInUseError: listenerError = ConnectionListener::AddressInUseError; break; default: // UnknownError break; } return listenerError; } } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/servercore/localsocketconnectionlistener.h000066400000000000000000000055511323436134600262160ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_LOCALSOCKETCONNECTIONLISTENER_H #define MOLEQUEUE_LOCALSOCKETCONNECTIONLISTENER_H #include "connectionlistener.h" #include class QLocalServer; class ServerTest; namespace MoleQueue { /** * @class LocalSocketConnectionListener localsocketconnectionlistener.h * * @brief Provides a implementation of ConnectionListener using QLocalServer. * Each connection made is emitted as a LocalSocketConnection. * * @see ConnectionListener */ class MOLEQUEUESERVERCORE_EXPORT LocalSocketConnectionListener : public ConnectionListener { Q_OBJECT public: /** * Constructor. * * @param parentObject parent * @param connectionString The address that the QLocalServer should listen on. */ explicit LocalSocketConnectionListener(QObject *parentObject, const QString &connectionString); /** * Destructor. */ ~LocalSocketConnectionListener(); /** * Start listening for incoming connections. * * @see ConnectionListener::start() */ void start(); /** * Stops the connection listener. * * @param force If true use QLocalServer::removeServer(...) to remove server * instance. * * @see ConnectionListener::stop(bool) */ void stop(bool force); /** * Calls stop(false) * * @see stop(bool) * @see ConnectionListener::stop() */ void stop(); /** * @return the address the QLocalServer is listening on. */ QString connectionString() const; /** * @return the full address the QLocalServer is listening on. */ QString fullConnectionString() const; /// Used for unit testing friend class ::ServerTest; private slots: /** * Called when a new connection is established by the QLocalServer. */ void newConnectionAvailable(); private: /// Method to map implementation specific error to generic errors. ConnectionListener::Error toConnectionListenerError( QAbstractSocket::SocketError error); /// The address the QLocalServer is listening on. QString m_connectionString; /// The internal local socket server QLocalServer *m_server; }; } // namespace MoleQueue #endif // MOLEQUEUE_LOCALSOCKETCONNECTIONLISTENER_H molequeue-0.9.0/molequeue/servercore/message.cpp000066400000000000000000000326241323436134600220450ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "message.h" #include "connection.h" #include "messageidmanager_p.h" #include #include #include #include namespace MoleQueue { // Dummy value returned by *Ref() functions called with invalid types. QJsonValue dummyValue(QJsonValue::Null); Message::Message(Connection *conn, EndpointIdType endpoint_) : m_type(Invalid), m_errorCode(0), m_connection(conn), m_endpoint(endpoint_) { } Message::Message(Message::MessageType type_, Connection *conn, EndpointIdType endpoint_) : m_type(type_), m_errorCode(0), m_connection(conn), m_endpoint(endpoint_) { } Message::Message(const QJsonObject &rawJson, Connection *conn, EndpointIdType endpoint_) : m_type(Raw), m_errorCode(0), m_rawJson(rawJson), m_connection(conn), m_endpoint(endpoint_) { } Message::Message(const Message &other) : m_type(other.m_type), m_method(other.m_method), m_id(other.m_id), m_params(other.m_params), m_result(other.m_result), m_errorCode(other.m_errorCode), m_errorMessage(other.m_errorMessage), m_errorData(other.m_errorData), m_rawJson(other.m_rawJson), m_connection(other.m_connection), m_endpoint(other.m_endpoint) { } Message& Message::operator=(const Message &other) { m_type = other.m_type; m_method = other.m_method; m_id = other.m_id; m_params = other.m_params; m_result = other.m_result; m_errorCode = other.m_errorCode; m_errorMessage = other.m_errorMessage; m_errorData = other.m_errorData; m_rawJson = other.m_rawJson; m_connection = other.m_connection; m_endpoint = other.m_endpoint; return *this; } Message::MessageType Message::type() const { return m_type; } QString Message::method() const { if (!checkType(Q_FUNC_INFO, Request | Notification | Response | Error)) return QString(); return m_method; } void Message::setMethod(const QString &m) { if (!checkType(Q_FUNC_INFO, Request | Notification | Response | Error)) return; m_method = m; } QJsonValue Message::params() const { if (!checkType(Q_FUNC_INFO, Request | Notification)) return QJsonValue(); return m_params; } QJsonValue& Message::paramsRef() { if (!checkType(Q_FUNC_INFO, Request | Notification)) return dummyValue; return m_params; } void Message::setParams(const QJsonArray &p) { if (!checkType(Q_FUNC_INFO, Request | Notification)) return; m_params = p; } void Message::setParams(const QJsonObject &p) { if (!checkType(Q_FUNC_INFO, Request | Notification)) return; m_params = p; } QJsonValue Message::result() const { if (!checkType(Q_FUNC_INFO, Response)) return QJsonValue(); return m_result; } QJsonValue& Message::resultRef() { if (!checkType(Q_FUNC_INFO, Response)) return dummyValue; return m_result; } void Message::setResult(const QJsonValue &r) { if (!checkType(Q_FUNC_INFO, Response)) return; m_result = r; } int Message::errorCode() const { if (!checkType(Q_FUNC_INFO, Error)) return 0; return m_errorCode; } void Message::setErrorCode(int e) { if (!checkType(Q_FUNC_INFO, Error)) return; m_errorCode = e; } QString Message::errorMessage() const { if (!checkType(Q_FUNC_INFO, Error)) return QString(); return m_errorMessage; } void Message::setErrorMessage(const QString &e) { if (!checkType(Q_FUNC_INFO,Error)) return; m_errorMessage = e; } QJsonValue Message::errorData() const { if (!checkType(Q_FUNC_INFO, Error)) return QJsonValue(); return m_errorData; } QJsonValue& Message::errorDataRef() { if (!checkType(Q_FUNC_INFO, Error)) return dummyValue; return m_errorData; } void Message::setErrorData(const QJsonValue &e) { if (!checkType(Q_FUNC_INFO, Error)) return; m_errorData = e; } MessageIdType Message::id() const { if (!checkType(Q_FUNC_INFO, Request | Response | Error)) return MessageIdType(); return m_id; } void Message::setId(const MessageIdType &i) { if (!checkType(Q_FUNC_INFO, Request | Response | Error)) return; m_id = i; } Connection* Message::connection() const { return m_connection; } void Message::setConnection(Connection* c) { m_connection = c; } EndpointIdType Message::endpoint() const { return m_endpoint; } void Message::setEndpoint(const EndpointIdType &e) { m_endpoint = e; } QJsonObject Message::toJsonObject() const { QJsonObject obj; switch (m_type) { case MoleQueue::Message::Request: obj.insert("jsonrpc", QLatin1String("2.0")); obj.insert("method", m_method); if ((m_params.isObject() && !m_params.toObject().isEmpty()) || (m_params.isArray() && !m_params.toArray().isEmpty())) { obj.insert("params", m_params); } obj.insert("id", m_id); break; case MoleQueue::Message::Notification: obj.insert("jsonrpc", QLatin1String("2.0")); obj.insert("method", m_method); if ((m_params.isObject() && !m_params.toObject().isEmpty()) || (m_params.isArray() && !m_params.toArray().isEmpty())) { obj.insert("params", m_params); } break; case MoleQueue::Message::Response: obj.insert("jsonrpc", QLatin1String("2.0")); obj.insert("result", m_result); obj.insert("id", m_id); break; case MoleQueue::Message::Error: { QJsonObject errorObject; errorObject.insert("code", m_errorCode); errorObject.insert("message", m_errorMessage); if (!m_errorData.isNull()) errorObject.insert("data", m_errorData); obj.insert("jsonrpc", QLatin1String("2.0")); obj.insert("error", errorObject); obj.insert("id", m_id); } break; case MoleQueue::Message::Raw: obj = m_rawJson; break; case MoleQueue::Message::Invalid: qWarning() << "Cannot convert invalid message to a JSON object."; break; } return obj; } PacketType Message::toJson() const { QJsonDocument doc(toJsonObject()); return PacketType(doc.toJson()); } bool Message::send() { if (m_type == Invalid || !m_connection || !m_connection->isOpen()) return false; if (m_type == Request) m_id = MessageIdManager::registerMethod(m_method); return m_connection->send(toJson(), m_endpoint); } Message Message::generateResponse() const { if (!checkType(Q_FUNC_INFO, Request)) return Message(); Message resp(Response, m_connection, m_endpoint); resp.m_method = m_method; resp.m_id = m_id; return resp; } Message Message::generateErrorResponse() const { if (!checkType(Q_FUNC_INFO, Request | Raw | Invalid)) return Message(); Message resp(Error, m_connection, m_endpoint); resp.m_method = m_method; resp.m_id = m_id; return resp; } bool Message::parse() { Message message; return parse(message); } bool Message::parse(Message &errorMessage_) { // Can only parse Raw types -- return true if this message is already parsed // or invalid. if (m_type != Raw) return true; // Validate the message QStringList errors; // jsonrpc must equal "2.0" if (!m_rawJson.contains("jsonrpc")) errors << "jsonrpc key missing."; if (!m_rawJson.value("jsonrpc").isString()) errors << "jsonrpc key must be a string."; if (m_rawJson.value("jsonrpc").toString() != "2.0") { errors << QString("Unrecognized jsonrpc string: %1") .arg(m_rawJson.value("jsonrpc").toString()); } // Must have either id or method if (!m_rawJson.contains("id") && !m_rawJson.contains("method")) errors << "Missing both id and method."; // If method is present, it must be a string. QString method_; if (m_rawJson.contains("method")) { if (!m_rawJson.value("method").isString()) errors << "method must be a string."; else method_ = m_rawJson.value("method").toString(); } else { // Lookup method for response/error. method_ = MessageIdManager::lookupMethod(m_rawJson.value("id")); } // If any errors have occurred, prep the response: if (!errors.empty()) { errors.prepend("Invalid request:"); QJsonObject errorDataObject; errorDataObject.insert("description", errors.join(" ")); errorDataObject.insert("request", m_rawJson); errorMessage_ = generateErrorResponse(); errorMessage_.setErrorCode(-32600); errorMessage_.setErrorMessage("Invalid request"); errorMessage_.setErrorData(errorDataObject); return false; } // Results, errors, and notifications cannot return errors. Parse them // as best we can and return true. if (m_rawJson.contains("result")) { interpretResponse(m_rawJson, method_); return true; } else if (m_rawJson.contains("error")) { interpretError(m_rawJson, method_); return true; } else if (!m_rawJson.contains("id")) { interpretNotification(m_rawJson); return true; } // Assume anything else is a request. return interpretRequest(m_rawJson, errorMessage_); } inline bool Message::checkType(const char *method_, MessageTypes validTypes) const { if (m_type & validTypes) return true; qWarning() << "Invalid message type in call.\n" << " Method:" << method_ << "\n" << " Valid types:" << validTypes << "\n" << " Actual type:" << m_type; return false; } bool Message::interpretRequest(const QJsonObject &json, Message &errorMessage_) { QStringList errors; // method must exist and be a string. if (!json.value("method").isString()) errors << "method is not a string."; // id must be present. if (!json.contains("id")) errors << "id missing."; // params is optional, but must be structured if present. if (json.contains("params") && !json.value("params").isArray() && !json.value("params").isObject()) { errors << "params must be structured if present."; } if (!errors.empty()) { errors.prepend("Invalid request:"); QJsonObject errorDataObject; errorDataObject.insert("description", errors.join(" ")); errorDataObject.insert("request", json); errorMessage_ = generateErrorResponse(); errorMessage_.setErrorCode(-32600); errorMessage_.setErrorMessage("Invalid request"); errorMessage_.setErrorData(errorDataObject); return false; } m_type = Request; m_method = json.value("method").toString(); if (json.contains("params")) m_params = json.value("params"); else m_params = QJsonValue(); m_id = MessageIdType(json.value("id")); return true; } void Message::interpretNotification(const QJsonObject &json) { m_type = Notification; m_method = json.value("method").toString(); if (json.contains("params")) m_params = json.value("params"); else m_params = QJsonValue(); m_id = MessageIdType(); return; } void Message::interpretResponse(const QJsonObject &json, const QString &method_) { m_type = Response; m_method = method_; m_result = json.value("result"); m_id = json.value("id"); return; } void Message::interpretError(const QJsonObject &json, const QString &method_) { m_type = Error; m_method = method_; m_id = json.value("id"); // We cannot send an error reply if we receive a malformated error message. // If this happens, generate a server error (-32000) with the original error // member as the error data (there must be an error member defined for this // function to be called) QStringList errors; QJsonValue errorValue = json.value("error"); if (!errorValue.isObject()) { errors << "error must be an object."; } else { QJsonObject errorObject = errorValue.toObject(); // error.code validation if (!errorObject.contains("code")) { errors << "error.code missing."; } else { // Check that error.code is integral. There is no QJsonValue.isInt() // method, only isDouble, so the test is more complicated than it should // be... if (!errorObject.value("code").isDouble()) { errors << "error.code is not numeric."; } else { double code = errorObject.value("code").toDouble(); if (qAbs(code - static_cast(static_cast(code))) > 1e-5) errors << "error.code is not integral."; else m_errorCode = static_cast(code); } } // error.message validation if (!errorObject.contains("message")) { errors << "error.message missing."; } else { if (!errorObject.value("message").isString()) errors << "error.message is not a string."; else m_errorMessage = errorObject.value("message").toString(); } if (errorObject.contains("data")) m_errorData = errorObject.value("data"); } // If any errors occured, reset the error members to a server error. if (!errors.empty()) { m_errorCode = -32000; m_errorMessage = "Server error"; QJsonObject errorDataObject; errors.prepend("Malformed error response:"); errorDataObject.insert("description", errors.join(" ")); errorDataObject.insert("origMessage", errorValue); m_errorData = errorDataObject; } } } // namespace MoleQueue molequeue-0.9.0/molequeue/servercore/message.h000066400000000000000000000267111323436134600215120ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_MESSAGE_H #define MOLEQUEUE_MESSAGE_H #include "molequeueservercoreexport.h" #include "servercoreglobal.h" #include #include #include #include class MessageTest; class ServerTest; namespace MoleQueue { class Connection; class JsonRpc; /** * @class Message message.h * @brief The Message class encaspulates a single JSON-RPC transmission. * @author David C. Lonie * * The Message class provides an interface to construct, interpret, and * manipulate JSON-RPC messages. * * There are four types of valid JSON-RPC messages: Requests, notifications, * responses, and errors. The type() method can be used to determine a given * Message's MessageType. A subset of the Message API is valid for each type; * the allowed attributes are dependent on the message type: * * - Request * - id * - method * - params * - Notification * - method * - params * - Response * - id * - method (may be empty -- used only for convenience, not part of JSON-RPC * specification). * - result * - Error * - id * - method (may be empty -- used only for convenience, not part of JSON-RPC * specification). * - errorCode * - errorMessage * - errorData * * Attempting to access an attribute that is invalid for the current type * will cause a warning to be printed and a default-constructed value is * returned. * * A Message may be constructed from a QJsonObject by using the QJsonObject * constructor and calling parse(). See the parse() documentation for more * details. * * When handling a Request Message, the generateResponse() and * generateErrorResponse() methods may be used to easily construct an empty * reply with the method, id, connection, and endpoint of the request. * * Once a message is ready to send, call the send() method. This will assign * and set a unique id to outgoing requests and call Connection::send() with a * JSON representation of the Message. If the application needs to track the id * of a request in order to identify the reply, record the id after calling * send(). * * The Request ids and methods are stored in an internal lookup table upon * sending. This is used to set the method of the incoming reply. If the lookup * fails, the message will be parsed properly, but the method attribute will not * be set. * * The JSON representation can be generated * and obtained by calling toJson(), and a QJsonObject representation is * available from the toJsonObject() method. */ class MOLEQUEUESERVERCORE_EXPORT Message { public: // Used for unit testing: friend class ::MessageTest; friend class ::ServerTest; /// Flags representing different types of JSON-RPC messages enum MessageType { /// A JSON-RPC request, with id, method, and params attributes. Request = 0x1, /// A JSON-RPC notification, with method and params attributes. Notification = 0x2, /// A JSON-RPC response, with id, method, and result attributes. Response = 0x4, /// A JSON-RPC error, with id, method, and errorCode, errorMessage, and /// errorData attributes. Error = 0x8, /// This MessageType indicates that this Message holds a raw QJsonObject /// that has not been interpreted. Call parse() to convert this Message /// into an appropriate type. Raw = 0x10, /// This Message is invalid. Invalid = 0x20 }; Q_DECLARE_FLAGS(MessageTypes, MessageType) /// Construct an Invalid Message using the @a conn and @a endpoint_. Message(Connection *conn = NULL, EndpointIdType endpoint_ = EndpointIdType()); /// Construct an empty Message with the specified @a type that uses the /// @a conn and @a endpoint_. Message(MessageType type_, Connection *conn = NULL, EndpointIdType endpoint_ = EndpointIdType()); /// Construct a Raw Message with the specified @a type that uses the /// @a conn and @a endpoint_. The @a rawJson QJsonObject will be cached to be /// parsed by parse() later. Message(const QJsonObject &rawJson, Connection *conn = NULL, EndpointIdType endpoint_ = EndpointIdType()); /// Copy constructor Message(const Message &other); /// Assignment operator Message &operator=(const Message &other); /// @return The MessageType of this Message. MessageType type() const; /** * @{ * The name of the method used in the remote procedure call. * @note This function is only valid for Request, Notification, Response, and * Error messages. */ QString method() const; void setMethod(const QString &m); /**@}*/ /** * @{ * The parameters used in the remote procedure call. * @note This function is only valid for Request and Notification messages. */ QJsonValue params() const; QJsonValue& paramsRef(); void setParams(const QJsonArray &p); void setParams(const QJsonObject &p); /**@}*/ /** * @{ * The result object used in a remote procedure call response. * @note This function is only valid for Response messages. */ QJsonValue result() const; QJsonValue& resultRef(); void setResult(const QJsonValue &r); /**@}*/ /** * @{ * The integral error code used in a remote procedure call error response. * @note This function is only valid for Error messages. */ int errorCode() const; void setErrorCode(int e); /**@}*/ /** * @{ * The error message string used in a remote procedure call error response. * @note This function is only valid for Error messages. */ QString errorMessage() const; void setErrorMessage(const QString &e); /**@}*/ /** * @{ * The data object used in a remote procedure call error response. * @note This function is only valid for Error messages. */ QJsonValue errorData() const; QJsonValue& errorDataRef(); void setErrorData(const QJsonValue &e); /**@}*/ /** * @{ * The message id used in a remote procedure call. * @note This function is only valid for Request, Response, and Error * messages. */ MessageIdType id() const; protected: // Users should have no reason to set this: void setId(const MessageIdType &i); public: /**@}*/ /** * @{ * The connection associated with the remote procedure call. */ Connection* connection() const; void setConnection(Connection* c); /**@}*/ /** * @{ * The connection endpoint associated with the remote procedure call. */ EndpointIdType endpoint() const; void setEndpoint(const EndpointIdType &e); /**@}*/ /** * @return A QJsonObject representation of the remote procedure call. */ QJsonObject toJsonObject() const; /** * @return A string representation of the remote procedure call. */ PacketType toJson() const; /** * @brief Send the message to the associated connection and endpoint. * @return True on success, false on failure. * @note If this message is a Request, a unique id will be assigned prior to * sending. Use the id() method to retrieve the assigned id. The id is * registered internally to properly identify the peer's Response or Error * message. */ bool send(); /** * @brief Create a new Response message in reply to a * Request. The connection, endpoint, id, and method will be copied from * @a this Message. * @note This function is only valid for Request messages. */ Message generateResponse() const; /** * @brief Create a new Error message in reply to a Request. * The connection, endpoint, id, and method will be copied from @a this * Message. * @note This function is only valid for Request, Raw, and Invalid messages. */ Message generateErrorResponse() const; /** * @{ * @brief Interpret the raw QJsonObject passed to the constructor that * takes a QJsonObject argument. * @return True on success, false on failure. * @note This function is only valid for Raw messages. * * This function will intepret the string as JSON, detect the type of message, * and update this message's type, and populate the internal data structures. * * The function returns true if the message was successfully interpreted, and * false if any error occured during parsing/interpretation. If any errors * occurred, the optional Message reference argument will be overwritten with * an appropriate error response. The following JSON-RPC 2.0 standard errors * are detected: * * - -32600 Invalid request * - The message type could not be determined. * * The JsonRpc class will handle the following errors as message are received: * * - -32700 Parse error * - Invalid JSON received, an error occurred during parsing. * - -32603 Internal error * - Internal JSON-RPC error * * The remaining standard JSON-RPC error codes should be handled by the * application developer: * * - -32601 Method not found * - Method not supported by application * - -32602 Invalid params * - Inappropriate parameters supplied for requested method. * * This method is intended to be used as follows: @code QJsonObject jsonObject = ...; Message message(jsonObject, connection, endpoint); Message errorMessage; if (!message.parse(errorMessage)) errorMessage.send(); else handleValidMessage(message); @endcode * * The Request ids and methods are stored in an internal lookup table upon * sending. This is used to set the method of the incoming reply. If the * lookup fails, the message will be parsed properly, but the method attribute * will not be set. */ bool parse(); bool parse(Message &errorMessage_); /**@}*/ private: /** * @brief Validate the message type. * @param method String representation of the method calling this function. * @param validTypes Bitwise-or combination of allowed types. * @return True if the type is valid, false otherwise. * * A warning will be printed if the type is invalid. */ bool checkType(const char *method_, MessageTypes validTypes) const; /** * @{ * Helper functions for parse(). Validate and intepret @a json. @a * errorMessage is used for error handling. */ bool interpretRequest(const QJsonObject &json, Message &errorMessage); void interpretNotification(const QJsonObject &json); void interpretResponse(const QJsonObject &json, const QString &method_); void interpretError(const QJsonObject &json, const QString &method_); /**@}*/ /// Type of message MessageType m_type; /** * @{ * Data storage. */ QString m_method; MessageIdType m_id; QJsonValue m_params; QJsonValue m_result; int m_errorCode; QString m_errorMessage; QJsonValue m_errorData; QJsonObject m_rawJson; /**@}*/ /// Connection from which the message originated Connection *m_connection; /// Used internally by Connection subclasses. EndpointIdType m_endpoint; }; Q_DECLARE_OPERATORS_FOR_FLAGS(Message::MessageTypes) } // namespace MoleQueue #endif // MOLEQUEUE_MESSAGE_H molequeue-0.9.0/molequeue/servercore/messageidmanager_p.cpp000066400000000000000000000027111323436134600242260ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "messageidmanager_p.h" #include "message.h" #include namespace MoleQueue { MessageIdManager *MessageIdManager::m_instance = NULL; MessageIdManager::MessageIdManager() : m_generator(0) { // Clean up when program exits. atexit(&cleanup); } void MessageIdManager::init() { if (!m_instance) m_instance = new MessageIdManager(); } void MessageIdManager::cleanup() { delete m_instance; m_instance = NULL; } MessageIdType MessageIdManager::registerMethod(const QString &method) { init(); double result = ++m_instance->m_generator; m_instance->m_lookup.insert(result, method); return MessageIdType(result); } QString MessageIdManager::lookupMethod(const MessageIdType &id) { init(); return id.isDouble() ? m_instance->m_lookup.take(id.toDouble()) : QString(); } } // end namespace MoleQueue molequeue-0.9.0/molequeue/servercore/messageidmanager_p.h000066400000000000000000000033421323436134600236740ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_MESSAGEIDMANAGER_P_H #define MOLEQUEUE_MESSAGEIDMANAGER_P_H #include "servercoreglobal.h" #include #include namespace MoleQueue { /** * @brief The MessageIdManager class provides a static lookup table that is used * to identify replies to JSON-RPC requests. * @author David C. Lonie */ class MessageIdManager { public: /** * @brief registerMethod Request a new message id that is assocated with @a * method. The new id and method will be registered in the lookup table. * @return The assigned message id. */ static MessageIdType registerMethod(const QString &method); /** * @brief lookupMethod Determine the method assocated with the @a id. * @note This removes the id from the internal lookup table. * @return The method assocated with the given id. */ static QString lookupMethod(const MessageIdType &id); private: MessageIdManager(); static void init(); static void cleanup(); static MessageIdManager *m_instance; QMap m_lookup; double m_generator; }; } #endif // MOLEQUEUE_MESSAGEIDMANAGER_P_H molequeue-0.9.0/molequeue/servercore/servercoreglobal.h000066400000000000000000000020731323436134600234210ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2013 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_SERVERCORE_SERVERCOREGLOBAL_H #define MOLEQUEUE_SERVERCORE_SERVERCOREGLOBAL_H #include #include namespace MoleQueue { /// Type for Endpoint identifiers typedef QByteArray EndpointIdType; /// Type for Message identifiers (JSON-RPC ids) typedef QJsonValue MessageIdType; /// Type for RPC packets typedef QByteArray PacketType; } #endif // MOLEQUEUE_SERVERCORE_SERVERCOREGLOBAL_H molequeue-0.9.0/molequeue/zeromq/000077500000000000000000000000001323436134600170445ustar00rootroot00000000000000molequeue-0.9.0/molequeue/zeromq/CMakeLists.txt000066400000000000000000000021241323436134600216030ustar00rootroot00000000000000find_package(Qt5 COMPONENTS Network REQUIRED) find_package(ZeroMQ REQUIRED) include_directories(SYSTEM ${ZeroMQ_INCLUDE_DIR}) include_directories(${CMAKE_CURRENT_BINARY_DIR}) include(GenerateExportHeader) add_definitions(-DUSE_ZERO_MQ) set(sources zeromqconnection.cpp zeromqconnectionlistener.cpp ) add_library(MoleQueueZeroMq ${sources}) qt5_use_modules(MoleQueueZeroMq Network) set_target_properties(MoleQueueZeroMq PROPERTIES AUTOMOC TRUE) target_link_libraries(MoleQueueZeroMq MoleQueueServerCore ${ZeroMQ_LIBRARIES}) generate_export_header(MoleQueueZeroMq EXPORT_FILE_NAME molequeuezeromqexport.h) include_directories(${CMAKE_CURRENT_BINARY_DIR}) set_property(TARGET MoleQueueZeroMq APPEND PROPERTY COMPILE_FLAGS ${molequeue_export_flags}) list(APPEND hdrs "${CMAKE_CURRENT_BINARY_DIR}/molequeuezeromqexport.h") install(FILES ${hdrs} DESTINATION "${INSTALL_INCLUDE_DIR}/molequeue/zeromq") install(TARGETS MoleQueueZeroMq EXPORT "MoleQueueTargets" RUNTIME DESTINATION ${INSTALL_RUNTIME_DIR} LIBRARY DESTINATION ${INSTALL_LIBRARY_DIR} ARCHIVE DESTINATION ${INSTALL_ARCHIVE_DIR}) molequeue-0.9.0/molequeue/zeromq/zeromqconnection.cpp000066400000000000000000000126671323436134600231610ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "zeromqconnection.h" #include #include namespace MoleQueue { const QString ZeroMqConnection::zeroMqPrefix = "zmq"; ZeroMqConnection::ZeroMqConnection(QObject *parentObject, zmq::context_t *context, zmq::socket_t *socket) : Connection(parentObject), m_context(context), m_socket(socket), m_connected(true), m_listening(false) { std::size_t socketTypeSize = sizeof(m_socketType); m_socket->getsockopt(ZMQ_TYPE, &m_socketType, &socketTypeSize); } ZeroMqConnection::ZeroMqConnection(QObject *parentObject, const QString &address) : Connection(parentObject), m_connectionString(address), m_context(new zmq::context_t(1)), m_socket(new zmq::socket_t(*m_context, ZMQ_DEALER)), m_connected(false) { m_socketType = ZMQ_DEALER; } ZeroMqConnection::~ZeroMqConnection() { close(); delete m_context; m_context = NULL; delete m_socket; m_socket = NULL; } void ZeroMqConnection::open() { if (m_socket) { QByteArray ba = m_connectionString.toLocal8Bit(); m_socket->connect(ba.data()); m_connected = true; } } void ZeroMqConnection::start() { if (!m_listening) { m_listening = true; QTimer::singleShot(0, this, SLOT(listen())); } } void ZeroMqConnection::close() { if (m_listening) { m_listening = false; m_socket->close(); } } bool ZeroMqConnection::isOpen() { return m_connected; } QString ZeroMqConnection::connectionString() const { return m_connectionString; } void ZeroMqConnection::listen() { if (m_listening) { // singleShotTimeout is the time (in ms) until the next call to this // function is posted to Qt's event loop. It is 500 ms is there is no // activity on the socket, 50 ms if a message was just received, and 0 // (e.g. immediate) if a message was just received and there are more // waiting. int singleShotTimeout = 500; int rc = 0; bool recvd = false; if (m_socketType == ZMQ_DEALER) recvd = dealerReceive(); else if (m_socketType == ZMQ_ROUTER) recvd = routerReceive(); else qWarning() << "Invalid socket type"; if (recvd) { // Message received -- lower the timeout and poll the socket for more data singleShotTimeout = 50; zmq::pollitem_t item[1]; item[0].socket = static_cast(*m_socket); item[0].events = ZMQ_POLLIN; try { rc = zmq::poll(item, 1, 0); } catch (zmq::error_t e) { qWarning("zmq exception during poll: Error %d: %s", e.num(), e.what()); } } // If there was more data, immediately post the next listen event. if (rc > 0) singleShotTimeout = 0; QTimer::singleShot(singleShotTimeout, this, SLOT(listen())); } } bool ZeroMqConnection::dealerReceive() { zmq::message_t message; if(m_socket->recv(&message, ZMQ_NOBLOCK)) { int size = message.size(); PacketType packet(static_cast(message.data()), size); emit packetReceived(packet, EndpointIdType()); return true; } return false; } bool ZeroMqConnection::routerReceive() { zmq::message_t address; if (m_socket->recv(&address, ZMQ_NOBLOCK)) { int size = address.size(); EndpointIdType replyTo(static_cast(address.data()), size); // Now receive the message zmq::message_t message; if(!m_socket->recv(&message, ZMQ_NOBLOCK)) { qWarning() << "Error no message body received"; return true; } PacketType packet(static_cast(message.data()), message.size()); emit packetReceived(packet, replyTo); return true; } return false; } bool ZeroMqConnection::send(const MoleQueue::PacketType &packet, const MoleQueue::EndpointIdType &endpoint) { zmq::message_t zmqMessage(packet.size()); memcpy(zmqMessage.data(), packet.constData(), packet.size()); bool rc; // If on the server side send the endpoint id first if (m_socketType == ZMQ_ROUTER) { zmq::message_t identity(endpoint.size()); memcpy(identity.data(), endpoint.data(), endpoint.size()); try { rc = m_socket->send(identity, ZMQ_SNDMORE | ZMQ_NOBLOCK); } catch (zmq::error_t e) { qWarning("zmq exception during endpoint send: Error %d: %s", e.num(), e.what()); return false; } if (!rc) { qWarning() << "zmq_send failed with EAGAIN"; return false; } } // Send message body try { rc = m_socket->send(zmqMessage, ZMQ_NOBLOCK); } catch (zmq::error_t e) { qWarning("zmq exception during message send: Error %d: %s", e.num(), e.what()); return false; } if (!rc) { qWarning() << "zmq_send failed with EAGAIN"; return false; } return true; } void ZeroMqConnection::flush() { // no op -- no flush command in zmq (http://www.zeromq.org/area:faq) } } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/zeromq/zeromqconnection.h000066400000000000000000000041331323436134600226130ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_ZEROMQCONNECTION_H #define MOLEQUEUE_ZEROMQCONNECTION_H #include "molequeuezeromqexport.h" #include #include class QTimer; namespace MoleQueue { /// @brief Connection subclass using ZeroMQ. class MOLEQUEUEZEROMQ_EXPORT ZeroMqConnection: public MoleQueue::Connection { Q_OBJECT public: ZeroMqConnection(QObject *parentObject, zmq::context_t *context, zmq::socket_t *socket); ZeroMqConnection(QObject *parentObject, const QString &address); ~ZeroMqConnection(); /** * Open the connection */ void open(); /** * Start receiving messages on this connection */ void start(); /** * Close the connection. Once a conneciton is closed if can't reused. */ void close(); /** * @return true, if the connection is open ( open has been called, * false otherwise */ bool isOpen(); /** * @return the connect string description the endpoint the connection is * connected to. */ QString connectionString() const; bool send(const PacketType &packet, const EndpointIdType &endpoint); void flush(); static const QString zeroMqPrefix; private slots: void listen(); private: bool dealerReceive(); bool routerReceive(); QString m_connectionString; zmq::context_t *m_context; zmq::socket_t *m_socket; int m_socketType; bool m_connected; bool m_listening; }; } // namespace MoleQueue #endif // MOLEQUEUE_ZEROMQCONNECTION_H molequeue-0.9.0/molequeue/zeromq/zeromqconnectionlistener.cpp000066400000000000000000000031571323436134600247210ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #include "zeromqconnectionlistener.h" #include #include #include namespace MoleQueue { ZeroMqConnectionListener::ZeroMqConnectionListener(QObject *parentObject, const QString &address) : ConnectionListener(parentObject), m_connectionString(address) { } void ZeroMqConnectionListener::start() { zmq::context_t *zeroContext = new zmq::context_t(1); zmq::socket_t *zeroSocket = new zmq::socket_t(*zeroContext, ZMQ_ROUTER); QByteArray ba = m_connectionString.toLocal8Bit(); zeroSocket->bind(ba.data()); ZeroMqConnection *connection = new ZeroMqConnection(this, zeroContext, zeroSocket); emit newConnection(connection); } void ZeroMqConnectionListener::stop(bool force) { Q_UNUSED(force) // Empty } void ZeroMqConnectionListener::stop() { // Empty } QString ZeroMqConnectionListener::connectionString() const { return m_connectionString; } } /* namespace MoleQueue */ molequeue-0.9.0/molequeue/zeromq/zeromqconnectionlistener.h000066400000000000000000000033521323436134600243630ustar00rootroot00000000000000/****************************************************************************** This source file is part of the MoleQueue project. Copyright 2012 Kitware, Inc. This source code is released under the New BSD License, (the "License"). 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. ******************************************************************************/ #ifndef MOLEQUEUE_ZEROMQCONNECTIONLISTENER_H #define MOLEQUEUE_ZEROMQCONNECTIONLISTENER_H #include #include "zeromqconnection.h" #include #include namespace MoleQueue { /// @brief ConnectionListener subclass using ZeroMQ. class MOLEQUEUEZEROMQ_EXPORT ZeroMqConnectionListener : public ConnectionListener { Q_OBJECT public: ZeroMqConnectionListener(QObject *parentObject, const QString &connectionString); /** * Start the connection listener, start listening for incoming connections. */ void start(); /** * Stop the connection listener. * * @param force if true, "extreme" measures may be taken to stop the listener. */ void stop(bool force); /** * Stop the connection listener without forcing it, equivalent to stop(false) * * @see stop(bool) */ void stop(); /** * @return the "address" the listener will listen on. */ QString connectionString() const; private: QString m_connectionString; }; } // namespace MoleQueue #endif // MOLEQUEUE_ZEROMQCONNECTIONLISTENER_H molequeue-0.9.0/python/000077500000000000000000000000001323436134600150475ustar00rootroot00000000000000molequeue-0.9.0/python/CMakeLists.txt000066400000000000000000000006401323436134600176070ustar00rootroot00000000000000find_package(PythonInterp REQUIRED) if(PYTHONINTERP_FOUND) execute_process(COMMAND ${PYTHON_EXECUTABLE} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())" OUTPUT_VARIABLE PYTHON_PACKAGES_DIR OUTPUT_STRIP_TRAILING_WHITESPACE) install(FILES molequeue/__init__.py molequeue/client.py molequeue/utils.py DESTINATION "${PYTHON_PACKAGES_DIR}/molequeue") endif() molequeue-0.9.0/python/molequeue/000077500000000000000000000000001323436134600170505ustar00rootroot00000000000000molequeue-0.9.0/python/molequeue/__init__.py000066400000000000000000000001611323436134600211570ustar00rootroot00000000000000from client import Client, Job, JobState, JobException, \ FilePath, FileContents, Queue, JobInformationException molequeue-0.9.0/python/molequeue/client.py000066400000000000000000000203061323436134600207010ustar00rootroot00000000000000import zmq from zmq.eventloop import ioloop from zmq.eventloop.zmqstream import ZMQStream from threading import Thread from threading import Condition from threading import Lock from functools import partial import inspect import json import time import tempfile import sys from utils import underscore_to_camelcase from utils import camelcase_to_underscore from utils import JsonRpc import threading class JobState: # Unknown status UNKNOWN = -1, # Initial state of job, should never be entered. NONE = 0, # Job has been accepted by the server and is being prepared (Writing input files, etc). ACCEPTED = 1 # Job is being queued locally, either waiting for local execution or remote submission. QUEUED_LOCAL = 2 # Job has been submitted to a remote queuing system. SUBMITTED = 3 # Job is pending execution on a remote queuing system. QUEUED_REMOTE = 4 # Job is running locally. RUNNING_LOCAL = 5 # Job is running remotely. RUNNING_REMOTE = 6 # Job has completed. FINISHED = 7 # Job has been terminated at a user request. CANCELED = 8 # Job has been terminated due to an error. ERROR = 9 class FilePath: def __init__(self): self.path = None class FileContents: def __init__(self): self.filename = None self.contents = None class Job: def __init__(self): self.queue = None self.program = None self.description = '' self.input_file = None self.output_directory = None self.local_working_directory = None self.clean_remote_files = False self.retrieve_output = True self.clean_local_working_directory = False self.hide_from_gui = False self.popup_on_state_change = True self.number_of_cores = 1 self.max_wall_time = -1 def job_state(self): return self._job_state def molequeue_id(self): return self._mole_queue_id def queue_id(self): return self._queue_id class Queue: def __init__(self): self.name = None; self.programs = []; class EventLoop(Thread): def __init__(self, io_loop): Thread.__init__(self) self.io_loop = io_loop def run(self): self.io_loop.start() def stop(self): self.io_loop.stop() self.join() class MoleQueueException(Exception): """The base class of all MoleQueue exceptions """ pass class JobException(MoleQueueException): def __init__(self, packet_id, code, message): self.packet_id = packet_id self.code = code self.message = message class JobInformationException(MoleQueueException): def __init__(self, packet_id, data, code, message): self.packet_id = packet_id self.data = data self.code = code self.message = message class Client: def __init__(self): self._current_packet_id = 0 self._request_response_map = {} self._new_response_condition = Condition() self._packet_id_lock = Lock() self._notification_callbacks = [] def connect_to_server(self, server): self.context = zmq.Context() self.socket = self.context.socket(zmq.DEALER) tmpdir = tempfile.gettempdir() connection_string = 'ipc://%s/%s_%s' % (tmpdir, 'zmq', server) self.socket.connect(connection_string) io_loop = ioloop.IOLoop.instance() self.stream = ZMQStream(self.socket, io_loop=io_loop) # create partial function that has self as first argument callback = partial(_on_recv, self) self.stream.on_recv(callback) self.event_loop = EventLoop(io_loop) self.event_loop.start() def disconnect(self): self.stream.flush() self.event_loop.stop() self.socket.close() def register_notification_callback(self, callback): # check a valid function has been past assert callable(callback) self._notification_callbacks.append(callback) def request_queue_list_update(self, timeout=None): packet_id = self._next_packet_id() jsonrpc = JsonRpc.generate_request(packet_id, 'listQueues', None) self._send_request(packet_id, jsonrpc) response = self._wait_for_response(packet_id, timeout) # Timeout if response == None: return None queues = JsonRpc.json_to_queues(response) return queues def submit_job(self, request, timeout=None): params = JsonRpc.object_to_json_params(request) packet_id = self._next_packet_id() jsonrpc = JsonRpc.generate_request(packet_id, 'submitJob', params) self._send_request(packet_id, jsonrpc) response = self._wait_for_response(packet_id, timeout) # Timeout if response == None: return None # if we an error occurred then throw an exception if 'error' in response: exception = JobException(response['id'], response['error']['code'], response['error']['message']) raise exception # otherwise return the molequeue id return response['result']['moleQueueId'] def cancel_job(self): # TODO pass def lookup_job(self, molequeue_id, timeout=None): params = {'moleQueueId': molequeue_id} packet_id = self._next_packet_id() jsonrpc = JsonRpc.generate_request(packet_id, 'lookupJob', params) self._send_request(packet_id, jsonrpc) response = self._wait_for_response(packet_id, timeout) # Timeout if response == None: return None # if we an error occurred then throw an exception if 'error' in response: exception = JobInformationException(response['id'], response['error']['data'], response['error']['code'], response['error']['message']) raise exception job = JsonRpc.json_to_job(response) return job def _on_response(self, packet_id, msg): if packet_id in self._request_response_map: self._new_response_condition.acquire() self._request_response_map[packet_id] = msg # notify any threads waiting that their response may have arrived self._new_response_condition.notify_all() self._new_response_condition.release() # TODO Convert raw JSON into a Python class def _on_notification(self, msg): for callback in self._notification_callbacks: callback(msg) # Testing only method. Kill the server application if allowed. def _send_rpc_kill_request(self, timeout=None): params = {} packet_id = self._next_packet_id() jsonrpc = JsonRpc.generate_request(packet_id, 'rpcKill', params) self._send_request(packet_id, jsonrpc) response = self._wait_for_response(packet_id, timeout) # Timeout if response == None: return None if 'result' in response and 'success' in response['result'] and response['result']['success'] == True: return True return False def _next_packet_id(self): with self._packet_id_lock: self._current_packet_id += 1 next = self._current_packet_id return next def _send_request(self, packet_id, jsonrpc): # add to request map so we know we are waiting on response for this packet # id self._request_response_map[packet_id] = None self.stream.send(str(jsonrpc)) self.stream.flush() def _wait_for_response(self, packet_id, timeout): try: start = time.time() # wait for the response to come in self._new_response_condition.acquire() while self._request_response_map[packet_id] == None: # need to set a wait time otherwise the wait can't be interrupted # See http://bugs.python.org/issue8844 wait_time = sys.maxint if timeout != None: wait_time = timeout - (time.time() - start) if wait_time <= 0: break; self._new_response_condition.wait(wait_time) response = self._request_response_map.pop(packet_id) self._new_response_condition.release() return response except KeyboardInterrupt: self.event_loop.stop() raise def _on_recv(client, msg): jsonrpc = json.loads(msg[0]) # reply to a request if 'id' in jsonrpc: packet_id = jsonrpc['id'] client._on_response(packet_id, jsonrpc) # this is a notification else: client._on_notification(jsonrpc) molequeue-0.9.0/python/molequeue/utils.py000066400000000000000000000032251323436134600205640ustar00rootroot00000000000000import json import re import itertools import types import molequeue class JsonRpc: INTERNAL_FIELDS = ['moleQueueId', 'queueId', 'jobState'] @staticmethod def generate_request(packet_id, method, parameters): request = {} request['jsonrpc'] = "2.0" request['id'] = packet_id request['method'] = method if parameters != None: request['params'] = parameters return json.dumps(request) @staticmethod def json_to_job(json): job = molequeue.Job() # convert response into Job object for key, value in json['result'].iteritems(): field = camelcase_to_underscore(key) if key in JsonRpc.INTERNAL_FIELDS: field = '_' + field job.__dict__[field] = value return job @staticmethod def json_to_queues(json): queues = [] for name, programs in json['result'].iteritems(): queue = molequeue.Queue(); queue.name = name queue.programs = programs queues.append(queue) return queues; @staticmethod def object_to_json_params(job): params = {} for key, value in job.__dict__.iteritems(): field = underscore_to_camelcase(key) if type(value) == types.InstanceType: value = JsonRpc.object_to_json_params(value) params[field] = value return params def underscore_to_camelcase(value): def camelcase(): yield str.lower while True: yield str.capitalize c = camelcase() return ''.join(c.next()(x) for x in value.split('_')) def camelcase_to_underscore(value): operation = itertools.cycle((lambda x : x.lower(), lambda x : '_' + x.lower())) return ''.join(operation.next()(x) for x in re.split('([A-Z])', value)) molequeue-0.9.0/python/test/000077500000000000000000000000001323436134600160265ustar00rootroot00000000000000molequeue-0.9.0/python/test/clienttest.py000066400000000000000000000056561323436134600205720ustar00rootroot00000000000000import unittest from functools import partial import time import molequeue class TestClient(unittest.TestCase): def test_submit_job(self): client = molequeue.Client() client.connect_to_server('MoleQueue') job = molequeue.Job() job.queue = 'salix' job.program = 'sleep (testing)' file_path = molequeue.FilePath() file_path.path = "/tmp/test" job.input_file = file_path molequeue_id = client.submit_job(job) print "MoleQueue ID: ", molequeue_id self.assertTrue(isinstance(molequeue_id, int)) client.disconnect() def test_notification_callback(self): client = molequeue.Client() client.connect_to_server('MoleQueue') self.callback_count = 0 def callback_counter(testcase, msg): testcase.callback_count +=1 callback = partial(callback_counter, self) client.register_notification_callback(callback) client.register_notification_callback(callback) job = molequeue.Job() job.queue = 'salix' job.program = 'sleep (testing)' molequeue_id = client.submit_job(job) # wait for notification time.sleep(1) self.assertIs(self.callback_count, 2) client.disconnect() def test_wait_for_response_timeout(self): client = molequeue.Client() # Fake up the request client._request_response_map[1] = None start = time.time() response = client._wait_for_response(1, 3) end = time.time() self.assertEqual(response, None) self.assertEqual(int(end - start), 3) def test_lookup_job(self): client = molequeue.Client() client.connect_to_server('MoleQueue') expected_job = molequeue.Job() expected_job.queue = 'salix' expected_job.program = 'sleep (testing)' expected_job.description = 'This is a test job' expected_job.hide_from_gui = True expected_job.popup_on_state_change = False file_contents = molequeue.FileContents() file_contents.filename = 'test.in' file_contents.contents = 'Hello' expected_job.input_file = file_contents molequeue_id = client.submit_job(expected_job) job = client.lookup_job(molequeue_id) self.assertEqual(molequeue_id, job.molequeue_id()) self.assertEqual(job.job_state(), molequeue.JobState.ACCEPTED) self.assertEqual(job.queue_id(), None) self.assertEqual(job.queue, expected_job.queue) self.assertEqual(job.program, expected_job.program) self.assertEqual(job.description, expected_job.description) self.assertEqual(job.hide_from_gui, expected_job.hide_from_gui) self.assertEqual(job.popup_on_state_change, expected_job.popup_on_state_change) client.disconnect() def test_request_queue_list_update(self): client = molequeue.Client() client.connect_to_server('MoleQueue') queues = client.request_queue_list_update() for q in queues: print q.name, ", ", q.programs; client.disconnect() if __name__ == '__main__': unittest.main() molequeue-0.9.0/python/test/utilstest.py000066400000000000000000000012671323436134600204460ustar00rootroot00000000000000import unittest import molequeue.utils class TestUtils(unittest.TestCase): def setUp(self): unittest.TestCase.setUp(self) self.test_values = [('this_is_a_test', 'thisIsATest'), ('this', 'this')] def test_underscore_to_camelcase(self): for (underscores, camelcase) in self.test_values: self.assertEqual(molequeue.utils.underscore_to_camelcase(underscores), camelcase) def test_camelcase_to_underscore(self): for (underscores, camelcase) in self.test_values: self.assertEqual(molequeue.utils.camelcase_to_underscore(camelcase), underscores) if __name__ == '__main__': unittest.main() molequeue-0.9.0/scripts/000077500000000000000000000000001323436134600152155ustar00rootroot00000000000000molequeue-0.9.0/scripts/git-gerrit-push000077500000000000000000000026071323436134600202020ustar00rootroot00000000000000#!/usr/bin/env bash USAGE="[] [--no-topic] [--dry-run] [--]" OPTIONS_SPEC= SUBDIRECTORY_OK=Yes . "$(git --exec-path)/git-sh-setup" #----------------------------------------------------------------------------- remote='' refspecs='' no_topic='' dry_run='' # Parse the command line options. while test $# != 0; do case "$1" in --no-topic) no_topic=1 ;; --dry-run) dry_run=--dry-run ;; --) shift; break ;; -*) usage ;; *) test -z "$remote" || usage ; remote="$1" ;; esac shift done test $# = 0 || usage # Default remote. test -n "$remote" || remote="gerrit" if test -z "$no_topic"; then # Identify and validate the topic branch name. topic="$(git symbolic-ref HEAD | sed -e 's|^refs/heads/||')" if test "$topic" = "master"; then die 'Please name your topic: git checkout -b descriptive-name' fi refspecs="HEAD:refs/for/master/$topic" fi # Exit early if we have nothing to push. if test -z "$refspecs"; then echo "Nothing to push!" exit 0 fi # Fetch the current upstream master branch head. # This helps the computation of a minimal pack to push. echo "Fetching $remote master" fetch_out=$(git fetch "$remote" master 2>&1) || die "$fetch_out" # Push. Save output and exit code. echo "Pushing to $remote" push_stdout=$(git push --porcelain $dry_run "$remote" $refspecs); push_exit=$? echo "$push_stdout" # Reproduce the push exit code. exit $push_exit molequeue-0.9.0/scripts/gitsetup/000077500000000000000000000000001323436134600170615ustar00rootroot00000000000000molequeue-0.9.0/scripts/setup-for-development.sh000077500000000000000000000011021323436134600220120ustar00rootroot00000000000000#!/usr/bin/env bash # Make sure we are inside the repository. cd "${BASH_SOURCE%/*}/.." # Rebase master by default git config rebase.stat true git config branch.master.rebase true echo "Checking basic user information..." scripts/gitsetup/setup-user echo echo "Setting up git hooks..." scripts/gitsetup/setup-hooks echo echo "Setting up git aliases..." scripts/setup-git-aliases echo echo "Setting up Gerrit..." scripts/gitsetup/setup-gerrit || echo "Failed to setup Gerrit. Run this script again to retry." echo setup_version=1 git config hooks.setup ${setup_version} molequeue-0.9.0/scripts/setup-git-aliases000077500000000000000000000004071323436134600205040ustar00rootroot00000000000000#!/usr/bin/env bash GITCONFIG="git config" # General aliases that could be global ${GITCONFIG} alias.prepush 'log --graph --stat origin/master..' # Alias to push the current topic branch to Gerrit ${GITCONFIG} alias.gerrit-push "!bash scripts/git-gerrit-push"