lib/ass_launcher/support/connection_string.rb
module AssLauncher
module Support
# Implement 1C connection string
# Mixin for connection string classes
# @note All connection string class have methods for get and set values
# of defined fields. Methods have name as fields but in downcase
# All fields defined for connection string class retutn {#fields}
# @example
# cs = AssLauncher::Support::\
# ConnectionString.new('File="\\fileserver\accounting.ib"')
# cs.is #-> :file
# cs.is? :file #-> true
# cs.usr = 'username'
# cs.pwd = 'password'
# cmd = "1civ8.exe enterprise #{cs.to_cmd}"
# run_result = AssLauncher::Support::Shell.run_ass(cmd)
module ConnectionString
class Error < StandardError; end
class ParseError < StandardError; end
# Commonn connection string fields
COMMON_FIELDS = %w(Usr Pwd LicDstr prmod Locale Zn Uc)
# Fields for server-infobase
SERVER_FIELDS = %w(Srvr Ref)
# Fields for file-infobase
FILE_FIELDS = %w(File)
# Fields for infobase published on http server
HTTP_FIELDS = %w(Ws)
HTTP_WEB_AUTH_FIELDS = %w(Wsn Wsp)
# Proxy fields for accsess to infobase published on http server via proxy
PROXY_FIELDS = %w(WspAuto WspSrv WspPort WspUser WspPwd)
# Fields for makes server-infobase
IB_MAKER_FIELDS = %w(DBMS DBSrvr DB
DBUID DBPwd SQLYOffs
CrSQLDB SchJobDn SUsr SPwd
DBFormat DBPageSize)
# Values for DBMS field
DBMS_VALUES = %w(MSSQLServer PostgreSQL IBMDB2 OracleDatabase)
# Values for DBPageSize
DB_PAGE_SIZE_VALUES = %w{4k , 8k, 16k, 32k, 64k}
# Values for DBFormat
DB_FORMAT_VALUES = %w{8.2.14 8.3.8}
# Analyzes connect string and build suitable class
# @param connstr (see parse)
# @return [Server | File | Http] instanse
def self.new(connstr)
case connstr
when /(\W|\A)File\s*=\s*"/i then File.new(parse(connstr))
when /(\W|\A)Srvr\s*=\s*"/i then Server.new(parse(connstr))
when /(\W|\A)Ws\s*=\s*"/i then Http.new(parse(connstr))
else
fail ParseError, "Uncknown connstr `#{connstr}'"
end
end
# Parse connect string into hash.
# Connect string have format:
# 'Field1="Value";Field2="Value";'
# Quotes ' " ' in value of field escape as doble quote ' "" '.
# Fields name convert to downcase [Symbol]
# @example
# parse 'Field="""Value"""' -> {field: '"Value"'}
# @param connstr [String]
# @return [Hash]
def self.parse(connstr)
res = {}
connstr.split(';').each do |str|
str.strip!
res.merge!(parse_key_value str) unless str.empty?
end
res
end
def self.parse_key_value(str)
fail ParseError, "Invalid string #{str}" unless\
/\A\s*(?<field>\w+)\s*=\s*"(?<value>.*)"\s*\z/i =~ str
{ field.downcase.to_sym => value.gsub('""', '"') }
end
private_class_method :parse_key_value
# Return type of connection string
# :file, :server, :http
# @return [Symbol]
def is
self.class.name.split('::').last.downcase.to_sym
end
# Check connection string for type :file, :server, :http
# @param symbol [Symvol]
# @example
# if cs.is? :file
# #do for connect to the file infobase
# else
# raise "#{cs.is} unsupport
# end
def is?(symbol)
is == symbol
end
def to_hash
result = {}
fields.each do |f|
result[f.downcase.to_sym] = get_property(f)
end
result
end
def to_s(only_fields = nil)
only_fields ||= fields
result = ''
only_fields.each do |f|
result << "#{prop_to_s(f)};" unless get_property(f).to_s.empty?
end
result
end
# Convert connection string to array of 1C:Enterprise parameters.
# @return [Array] of 1C:Enterprise CLI parameters.
def to_args
to_args_common + to_args_private
end
def to_args_common
r = []
r += ['/N', usr] if usr
r += ['/P', pwd] if pwd
r += ['/UsePrivilegedMode', ''] if prmod.to_s == '1'
r += ['/L', locale] if locale
r += ['/Z', zn] if zn
r
end
private :to_args_common
# Convert connection string to string of 1C:Enterprise parameters
# like /N"usr" /P"pwd" etc. See {#to_args}
# @return [String]
def to_cmd
r = ''
args = to_args
args.each_with_index do |v, i|
next unless i.even?
r << v
r << "\"#{args[i + 1]}\"" unless args[i + 1].to_s.empty?
r << ' '
end
r
end
# Fields required for new instance of connection string
def required_fields
self.class.required_fields
end
# All fields defined for connection string
def fields
self.class.fields
end
def self.included(base)
base.fields.each do |f|
base.send(:attr_reader, f.downcase.to_sym) unless\
base.instance_methods.include? f.downcase.to_sym
base.send(:attr_writer, f.downcase.to_sym) unless\
base.instance_methods.include? "#{f.downcase}=".to_sym
end
end
private
def _set_properties(hash)
hash.each do |key, value|
set_property(key, value)
end
end
def set_property(prop, value)
send("#{prop.downcase}=".to_sym, value)
end
def get_property(prop)
send(prop.downcase.to_sym)
end
def prop_to_s(prop)
"#{fields_to_hash[prop.downcase.to_sym]}="\
"\"#{get_property(prop).to_s.gsub('"', '""')}\""
end
def fields_to_hash
res = {}
fields.each do |f|
res[f.downcase.to_sym] = f
end
res
end
def required_fields_received?(received_fields)
(required_fields.map { |f| f.downcase.to_sym }\
& received_fields.keys.map { |k| k.downcase.to_sym }) == \
required_fields.map { |f| f.downcase.to_sym }
end
# Connection string for server-infobases
# @note (see ConnectionString)
class Server
# Simple class host:port
class ServerDescr
attr_reader :host, :port
# @param host [String] hostname
# @param port [String] port number
def initialize(host, port = nil)
@host = host.strip
@port = port.to_s.strip
end
# Parse sting <srv_string>
# @param srv_str [String] string like 'host:port,host:port'
# @return [Arry<ServerDescr>]
def self.parse(srv_str)
r = []
srv_str.split(',').each do |srv|
srv.strip!
r << new(* srv.chomp.split(':')) unless srv.empty?
end
r
end
# @return [String] formated 'host:port'
def to_s
"#{host}" + (port.empty? ? '' : ":#{port}")
end
end
def self.fields
required_fields | COMMON_FIELDS | IB_MAKER_FIELDS
end
def self.required_fields
SERVER_FIELDS
end
def initialize(hash)
fail ConnectionString::Error unless required_fields_received?(hash)
_set_properties(hash)
end
# @return [Array<ServerDescr>]
def servers
@servers ||= []
end
def srvr=(str)
fail ArgumentError if str.empty?
@servers = ServerDescr.parse(str)
@srvr = str
end
def ref=(str)
fail ArgumentError if str.empty?
@ref = str
end
def srvr
servers.join(',')
end
def srvr_raw
@srvr
end
# Build string suitable for
# :createinfibase runmode
# @todo validte createinfibase params
def createinfobase_cmd
to_s(fields - %w{Usr Pwd})
end
# Build string suitable for Ole objects connecting.
def to_ole_string
"#{to_s(fields - IB_MAKER_FIELDS)}"
end
# Build args array suitable for
# :createinfibase runmode
def createinfobase_args
[createinfobase_cmd.gsub(%r{=\s*"},"='").gsub(%r{"\s*;},"';")]
end
# (see DB_PAGE_SIZE_VALUES)
def dbpagesize=(value)
@dbpagesize = valid_value(value, DB_PAGE_SIZE_VALUES)
end
# (see DB_FORMAT_VALUES)
def dbformat=(value)
@dbformat = valid_value(value, DB_FORMAT_VALUES)
end
# (see DBMS_VALUES)
def dbms=(value)
@dbms = valid_value(value, DBMS_VALUES)
end
def crsqldb=(value)
@crsqldb = yes_or_not(value)
end
def schjobdn=(value)
@schjobdn = yes_or_not(value)
end
def yes_or_not(value)
valid_value(value, %w(Y N))
end
private :yes_or_not
def valid_value(v, av)
fail ArgumentError,
"Bad value #{v}. Accepted values are `#{av}'" unless\
av.map(&:downcase).include?(v.to_s.downcase)
v
end
private :valid_value
def to_args_private
['/S', "#{srvr}/#{ref}"]
end
private :to_args_private
include ConnectionString
end
# Connection string for file-infobases
# @note (see ConnectionString)
class File
def self.required_fields
FILE_FIELDS
end
def self.fields
required_fields | COMMON_FIELDS
end
def initialize(hash)
fail ConnectionString::Error unless required_fields_received?(hash)
_set_properties(hash)
end
def file=(str)
fail ArgumentError if str.empty?
@file = str
end
# Build string suitable for
# :createinfibase runmode
def createinfobase_cmd
"File=\"#{path.realdirpath.win_string}\""
end
# Build string suitable for Ole objects connecting.
def to_ole_string
"#{createinfobase_cmd};#{to_s(fields - ['File'])}"
end
# Build args array suitable for
# :createinfibase runmode
# Fucking 1C:
# - File="path" not work but work running as script
# - File='path' work correct
def createinfobase_args
return ["File='#{path.win_string}'"] if path.relative?
["File='#{path.realdirpath.win_string}'"]
end
def path
AssLauncher::Support::Platforms.path(file)
end
# Convert connection string to array of 1C:Enterprise parameters.
# @return [Array] of 1C:Enterprise CLI parameters.
def to_args_private
['/F', path.realpath.to_s]
end
private :to_args_private
include ConnectionString
end
# Connection string for infobases published on http server
# @note (see ConnectionString)
class Http
def self.required_fields
HTTP_FIELDS
end
def self.fields
required_fields | COMMON_FIELDS | HTTP_WEB_AUTH_FIELDS | PROXY_FIELDS
end
def initialize(hash)
fail ConnectionString::Error unless required_fields_received?(hash)
_set_properties(hash)
end
def ws=(str)
fail ArgumentError if str.empty?
@ws = str
end
def uri
require 'uri'
uri = URI(ws)
uri.user = wsn
uri.password = wsp
q = uri_query
uri.query = escape q if q
uri
end
def escape(s)
AssLauncher::Enterprise::WebClient.escape s
end
private :escape
def uri_query
r = ''
r << "N=#{usr}&" if usr
r << "P=#{pwd}&" if pwd
r << "L=#{locale}&" if locale
r << "Z=#{zn}&" if zn
r << "UsePrivilegedMode&" if prmod
r.gsub!(/&$/i, '')
return nil if r.empty?
r
end
private :uri_query
# Convert connection string to array of 1C:Enterprise parameters.
# @return [Array] of 1C:Enterprise CLI parameters.
def to_args_private
r = []
r += ['/WS', ws] if ws
r += ['/WSN', wsn] if wsn
r += ['/WSP', wsp] if wsp
to_args_private_proxy(r)
end
private :to_args_private
def to_args_private_proxy(r)
return r unless !wspauto && wspsrv
r += ['/Proxy', '', '-Psrv', wspsrv]
r += ['-PPort', wspport.to_s] if wspport
r += ['-PUser', wspuser] if wspuser
r += ['-PPwd', wsppwd] if wsppwd
r
end
private :to_args_private_proxy
include ConnectionString
end
end
end
end