|
| 1 | +// Reusable JDBC URL parser and helpers |
| 2 | + |
| 3 | +export type JdbcVendor = 'MySQL' | 'PostgreSQL' | 'SQLServer' | 'Oracle' | 'SQLite' | 'H2' | 'Other' |
| 4 | + |
| 5 | +export interface JdbcHost { host: string; port?: number } |
| 6 | + |
| 7 | +export interface JdbcParsed { |
| 8 | + vendor: JdbcVendor |
| 9 | + hosts: JdbcHost[] |
| 10 | + database?: string |
| 11 | + params: Record<string, string> |
| 12 | + raw: string |
| 13 | +} |
| 14 | + |
| 15 | +export function getDefaultPort(vendor: JdbcVendor): number | undefined { |
| 16 | + switch (vendor) { |
| 17 | + case 'MySQL': return 3306 |
| 18 | + case 'PostgreSQL': return 5432 |
| 19 | + case 'SQLServer': return 1433 |
| 20 | + case 'Oracle': return 1521 |
| 21 | + default: return undefined |
| 22 | + } |
| 23 | +} |
| 24 | + |
| 25 | +function parseKv(str: string, sep: RegExp = /[&;]/): Record<string, string> { |
| 26 | + const out: Record<string, string> = {} |
| 27 | + if (!str) return out |
| 28 | + str.split(sep).forEach(pair => { |
| 29 | + if (!pair) return |
| 30 | + const i = pair.indexOf('=') |
| 31 | + if (i === -1) { |
| 32 | + const k = pair.trim() |
| 33 | + if (k) out[k] = '' |
| 34 | + return |
| 35 | + } |
| 36 | + const k = pair.slice(0, i).trim() |
| 37 | + const v = pair.slice(i + 1).trim() |
| 38 | + if (k) out[k] = v |
| 39 | + }) |
| 40 | + return out |
| 41 | +} |
| 42 | + |
| 43 | +function parseHostPort(token: string): JdbcHost { |
| 44 | + // IPv6: [fe80::1]:5432 |
| 45 | + const ipv6 = token.match(/^\[([^\]]+)\](?::(\d+))?$/) |
| 46 | + if (ipv6) { |
| 47 | + return { host: `[${ipv6[1]}]`, port: ipv6[2] ? Number(ipv6[2]) : undefined } |
| 48 | + } |
| 49 | + const idx = token.lastIndexOf(':') |
| 50 | + if (idx > -1 && token.indexOf(':') === idx) { |
| 51 | + const host = token.slice(0, idx) |
| 52 | + const port = Number(token.slice(idx + 1)) |
| 53 | + return isNaN(port) ? { host: token } : { host, port } |
| 54 | + } |
| 55 | + return { host: token } |
| 56 | +} |
| 57 | + |
| 58 | +export function parseJdbcUrl(url?: string): JdbcParsed { |
| 59 | + const raw = url ?? '' |
| 60 | + if (!raw) return { vendor: 'Other', hosts: [], database: undefined, params: {}, raw } |
| 61 | + |
| 62 | + // MySQL / PostgreSQL |
| 63 | + let m = raw.match(/^jdbc:(mysql|postgresql):\/\/([^\/]+)\/([^?;]+)(?:[?;](.*))?$/i) |
| 64 | + if (m) { |
| 65 | + const vendor: JdbcVendor = m[1].toLowerCase() === 'mysql' ? 'MySQL' : 'PostgreSQL' |
| 66 | + const hosts = m[2].split(',').map(h => parseHostPort(h.trim())).filter(Boolean) |
| 67 | + const database = decodeURIComponent(m[3]) |
| 68 | + const params = parseKv(m[4] || '') |
| 69 | + const def = getDefaultPort(vendor) |
| 70 | + hosts.forEach(h => { if (!h.port && def) h.port = def }) |
| 71 | + return { vendor, hosts, database, params, raw } |
| 72 | + } |
| 73 | + |
| 74 | + // SQLServer |
| 75 | + m = raw.match(/^jdbc:sqlserver:\/\/([^;]+)(?:;(.*))?$/i) |
| 76 | + if (m) { |
| 77 | + const host = parseHostPort(m[1].trim()) |
| 78 | + const params = parseKv(m[2] || '', /[;]+/) |
| 79 | + const database = params.databaseName || params['database'] || undefined |
| 80 | + if (!host.port) host.port = getDefaultPort('SQLServer') |
| 81 | + return { vendor: 'SQLServer', hosts: [host], database, params, raw } |
| 82 | + } |
| 83 | + |
| 84 | + // Oracle Thin (SID) |
| 85 | + m = raw.match(/^jdbc:oracle:thin:@([^:\/]+)(?::(\d+))?:(.+)$/i) |
| 86 | + if (m) { |
| 87 | + const host = { host: m[1], port: m[2] ? Number(m[2]) : getDefaultPort('Oracle') } |
| 88 | + const database = m[3] |
| 89 | + return { vendor: 'Oracle', hosts: [host], database, params: {}, raw } |
| 90 | + } |
| 91 | + // Oracle Thin (Service) |
| 92 | + m = raw.match(/^jdbc:oracle:thin:@\/\/([^\/:]+)(?::(\d+))?\/(.+)$/i) |
| 93 | + if (m) { |
| 94 | + const host = { host: m[1], port: m[2] ? Number(m[2]) : getDefaultPort('Oracle') } |
| 95 | + const database = m[3] |
| 96 | + return { vendor: 'Oracle', hosts: [host], database, params: {}, raw } |
| 97 | + } |
| 98 | + |
| 99 | + // SQLite |
| 100 | + m = raw.match(/^jdbc:sqlite:(.+)$/i) |
| 101 | + if (m) { |
| 102 | + return { vendor: 'SQLite', hosts: [], database: m[1], params: {}, raw } |
| 103 | + } |
| 104 | + |
| 105 | + // H2 |
| 106 | + m = raw.match(/^jdbc:h2:(mem|file):(.+)$/i) |
| 107 | + if (m) { |
| 108 | + return { vendor: 'H2', hosts: [], database: `${m[1]}:${m[2]}`, params: {}, raw } |
| 109 | + } |
| 110 | + |
| 111 | + return { vendor: 'Other', hosts: [], database: undefined, params: {}, raw } |
| 112 | +} |
| 113 | + |
| 114 | +export function getJdbcSummary(url?: string): string { |
| 115 | + const p = parseJdbcUrl(url) |
| 116 | + const v = p.vendor |
| 117 | + if (v === 'SQLite' || v === 'H2') { |
| 118 | + return p.database ? `${v} · ${p.database}` : v |
| 119 | + } |
| 120 | + if (p.hosts.length) { |
| 121 | + const first = p.hosts[0] |
| 122 | + const hostPort = `${first.host}${first.port ? `:${first.port}` : ''}` |
| 123 | + const multi = p.hosts.length > 1 ? ' 等' : '' |
| 124 | + const db = p.database ? ` · ${p.database}` : '' |
| 125 | + return `${v} · ${hostPort}${multi}${db}` |
| 126 | + } |
| 127 | + return p.raw || '' |
| 128 | +} |
| 129 | + |
| 130 | +export function getJdbcTooltip(url?: string): string { |
| 131 | + const p = parseJdbcUrl(url) |
| 132 | + const parts: string[] = [] |
| 133 | + parts.push(`类型:${p.vendor}`) |
| 134 | + if (p.hosts.length) { |
| 135 | + parts.push(`主机:${p.hosts.map(h => `${h.host}${h.port ? `:${h.port}` : ''}`).join(', ')}`) |
| 136 | + } |
| 137 | + if (p.database) parts.push(`库名:${p.database}`) |
| 138 | + const paramKeys = Object.keys(p.params || {}) |
| 139 | + if (paramKeys.length) parts.push(`参数:${paramKeys.length} 项`) |
| 140 | + parts.push(`完整:${p.raw}`) |
| 141 | + return parts.join('\n') |
| 142 | +} |
| 143 | + |
| 144 | +export function hasSsl(url?: string): boolean { |
| 145 | + const p = parseJdbcUrl(url) |
| 146 | + const lower: Record<string, string> = {} |
| 147 | + Object.keys(p.params).forEach(k => { |
| 148 | + lower[k.toLowerCase()] = (p.params[k] ?? '').toString().toLowerCase() |
| 149 | + }) |
| 150 | + if ('usessl' in lower) return lower['usessl'] === 'true' |
| 151 | + if ('ssl' in lower) return lower['ssl'] === 'true' |
| 152 | + if ('sslmode' in lower) return !!lower['sslmode'] && lower['sslmode'] !== 'disable' |
| 153 | + if ('encrypt' in lower) return lower['encrypt'] === 'true' |
| 154 | + return false |
| 155 | +} |
0 commit comments