pd4ml 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +91 -0
- data/docs/api.md +546 -0
- data/docs/guide.md +782 -0
- data/exe/pd4ml +132 -0
- data/lib/pd4ml/bridge.rb +362 -0
- data/lib/pd4ml/errors.rb +42 -0
- data/lib/pd4ml/installer.rb +183 -0
- data/lib/pd4ml/java.rb +209 -0
- data/lib/pd4ml/pd4ml-bridge.jar +0 -0
- data/lib/pd4ml/pd4ml.rb +184 -0
- data/lib/pd4ml/version.rb +5 -0
- data/lib/pd4ml.rb +169 -0
- metadata +58 -0
data/exe/pd4ml
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# The pd4ml command: install/list PD4ML builds, show the version, run a bridge server, convert.
|
|
5
|
+
require 'optparse'
|
|
6
|
+
require_relative '../lib/pd4ml'
|
|
7
|
+
|
|
8
|
+
USAGE = <<~TEXT
|
|
9
|
+
usage: pd4ml <command> [options]
|
|
10
|
+
|
|
11
|
+
install [VERSION] [--snapshots] [--jakarta] [--home DIR] [--force] [--no-bouncycastle]
|
|
12
|
+
download a PD4ML build (default: the most recent 4.* release), or update to it
|
|
13
|
+
list [--snapshots] [--jakarta]
|
|
14
|
+
the versions in the PD4ML repository (* the installed one)
|
|
15
|
+
version the PD4ML version the gem runs (downloads it on first use)
|
|
16
|
+
serve [--port N] run a bridge server (default port 8723) for PD4ML_BRIDGE=tcp://127.0.0.1:N
|
|
17
|
+
convert INPUT OUTPUT [--format pdf|docx|rtf|png] [--page-size A4] [--landscape]
|
|
18
|
+
[--margins 10,10,10,10] [--width PX] [--engine v42|v40] [--spec pdf/a-2b]
|
|
19
|
+
[--footer HTML] [--bookmarks]
|
|
20
|
+
convert a file or URL; the format follows OUTPUT's extension by default
|
|
21
|
+
|
|
22
|
+
PD4ML_CLASSPATH / PD4ML_CLASSPATH_FILE / PD4ML_HOME / PD4ML_JAVA choose the build and Java.
|
|
23
|
+
https://pd4ml.com/docs/
|
|
24
|
+
TEXT
|
|
25
|
+
|
|
26
|
+
def options(args, &block)
|
|
27
|
+
opts = {}
|
|
28
|
+
parser = OptionParser.new { |o| block.call(o, opts) }
|
|
29
|
+
rest = parser.parse(args)
|
|
30
|
+
[opts, rest]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def convert(args)
|
|
34
|
+
opts, (input, output) = options(args) do |o, h|
|
|
35
|
+
o.on('--format F') { |v| h[:format] = v }
|
|
36
|
+
o.on('--page-size S') { |v| h[:page_size] = v }
|
|
37
|
+
o.on('--landscape') { h[:landscape] = true }
|
|
38
|
+
o.on('--margins M') { |v| h[:margins] = v }
|
|
39
|
+
o.on('--width PX', Integer) { |v| h[:width] = v }
|
|
40
|
+
o.on('--engine E') { |v| h[:engine] = v }
|
|
41
|
+
o.on('--spec S') { |v| h[:spec] = v }
|
|
42
|
+
o.on('--footer HTML') { |v| h[:footer] = v }
|
|
43
|
+
o.on('--bookmarks') { h[:bookmarks] = true }
|
|
44
|
+
end
|
|
45
|
+
raise ArgumentError, 'convert: INPUT and OUTPUT expected' unless input && output
|
|
46
|
+
|
|
47
|
+
format = (opts[:format] || File.extname(output).delete('.')).downcase
|
|
48
|
+
pd = Pd4ml::PD4ML.new(html_width: opts[:width], engine: opts[:engine])
|
|
49
|
+
if opts[:page_size]
|
|
50
|
+
size = Pd4ml::PageSize.get_instance(opts[:page_size].downcase)
|
|
51
|
+
raise ArgumentError, "unknown page size '#{opts[:page_size]}'" if size.nil?
|
|
52
|
+
|
|
53
|
+
pd.set_page_size(opts[:landscape] ? size.rotate : size)
|
|
54
|
+
elsif opts[:landscape]
|
|
55
|
+
pd.set_page_size(Pd4ml::PageSize::A4.rotate)
|
|
56
|
+
end
|
|
57
|
+
if opts[:margins]
|
|
58
|
+
mm = opts[:margins].split(',').map { |s| Integer(s.strip) }
|
|
59
|
+
mm *= 4 if mm.size == 1
|
|
60
|
+
raise ArgumentError, '--margins: one or four values (mm)' unless mm.size == 4
|
|
61
|
+
|
|
62
|
+
pd.set_page_margins(Pd4ml::PageMargins.new(*mm, Pd4ml::Units::MM))
|
|
63
|
+
end
|
|
64
|
+
pd.set_page_footer(opts[:footer], 30, '1+') if opts[:footer]
|
|
65
|
+
pd.generate_bookmarks_from_headings(true) if opts[:bookmarks]
|
|
66
|
+
pd.read_html(input)
|
|
67
|
+
case format
|
|
68
|
+
when 'pdf' then pd.write_pdf(output, spec: opts[:spec])
|
|
69
|
+
when 'docx' then pd.write_docx(output)
|
|
70
|
+
when 'rtf' then pd.write_rtf(output)
|
|
71
|
+
when 'png'
|
|
72
|
+
pages = pd.render_png
|
|
73
|
+
if pages.size == 1
|
|
74
|
+
File.binwrite(output, pages[0])
|
|
75
|
+
else
|
|
76
|
+
stem = output.sub(/\.png\z/i, '')
|
|
77
|
+
pages.each_with_index { |p, i| File.binwrite("#{stem}-#{i + 1}.png", p) }
|
|
78
|
+
end
|
|
79
|
+
else raise ArgumentError, "unknown format '#{format}' (pdf, docx, rtf, png)"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def main(argv)
|
|
84
|
+
command, *args = argv
|
|
85
|
+
if command.nil? || %w[-h --help help].include?(command)
|
|
86
|
+
puts USAGE
|
|
87
|
+
return command ? 0 : 2
|
|
88
|
+
end
|
|
89
|
+
case command
|
|
90
|
+
when 'install'
|
|
91
|
+
opts, rest = options(args) do |o, h|
|
|
92
|
+
o.on('--snapshots') { h[:snapshots] = true }
|
|
93
|
+
o.on('--jakarta') { h[:jakarta] = true }
|
|
94
|
+
o.on('--home DIR') { |v| h[:home] = v }
|
|
95
|
+
o.on('--force') { h[:force] = true }
|
|
96
|
+
o.on('--no-bouncycastle') { h[:bouncycastle] = false }
|
|
97
|
+
end
|
|
98
|
+
puts Pd4ml::Installer.install(rest.first || 'latest', **opts)
|
|
99
|
+
when 'list'
|
|
100
|
+
opts, = options(args) do |o, h|
|
|
101
|
+
o.on('--snapshots') { h[:snapshots] = true }
|
|
102
|
+
o.on('--jakarta') { h[:jakarta] = true }
|
|
103
|
+
end
|
|
104
|
+
installed = Pd4ml::Installer.installed_version
|
|
105
|
+
Pd4ml::Installer.sort_versions(Pd4ml::Installer.available_versions).each do |v|
|
|
106
|
+
next if !opts[:snapshots] && v.end_with?('-SNAPSHOT')
|
|
107
|
+
next if v.include?('-jakarta') != !!opts[:jakarta]
|
|
108
|
+
|
|
109
|
+
puts((v == installed ? '* ' : ' ') + v)
|
|
110
|
+
end
|
|
111
|
+
when 'version'
|
|
112
|
+
puts Pd4ml.version
|
|
113
|
+
when 'serve'
|
|
114
|
+
opts, = options(args) { |o, h| o.on('--port N', Integer) { |v| h[:port] = v } }
|
|
115
|
+
port = opts[:port] || 8723
|
|
116
|
+
cmd = Pd4ml::Bridge.command(port: port)
|
|
117
|
+
warn "PD4ML bridge on tcp://127.0.0.1:#{port} (Ctrl+C to stop)"
|
|
118
|
+
exec(*cmd)
|
|
119
|
+
when 'convert'
|
|
120
|
+
convert(args)
|
|
121
|
+
else
|
|
122
|
+
warn "pd4ml: unknown command '#{command}'\n\n#{USAGE}"
|
|
123
|
+
return 2
|
|
124
|
+
end
|
|
125
|
+
0
|
|
126
|
+
rescue Pd4ml::BridgeError, Pd4ml::InstallError, Pd4ml::JavaError, ArgumentError, OptionParser::ParseError,
|
|
127
|
+
SystemCallError => e
|
|
128
|
+
warn "pd4ml: #{e.message}"
|
|
129
|
+
1
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
exit main(ARGV)
|
data/lib/pd4ml/bridge.rb
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'monitor'
|
|
5
|
+
require 'open3'
|
|
6
|
+
require 'socket'
|
|
7
|
+
require 'uri'
|
|
8
|
+
require 'pathname'
|
|
9
|
+
|
|
10
|
+
module Pd4ml
|
|
11
|
+
# An OutputStream argument whose content comes back to Ruby (see Bridge#request).
|
|
12
|
+
Output = Struct.new(:index)
|
|
13
|
+
|
|
14
|
+
# The connection to the Java side: a bridge process (pd4ml-bridge.jar, started on first use) or
|
|
15
|
+
# a bridge server (`pd4ml serve`, PD4ML_BRIDGE=tcp://host:port). One shared bridge serves the
|
|
16
|
+
# whole process; it is thread-safe, and a callback may call PD4ML itself.
|
|
17
|
+
class Bridge
|
|
18
|
+
BRIDGE_JAR = File.expand_path('pd4ml-bridge.jar', __dir__)
|
|
19
|
+
|
|
20
|
+
@options = {}
|
|
21
|
+
@default = nil
|
|
22
|
+
@lock = Monitor.new
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
# Sets up the shared bridge; call before the first use of PD4ML.
|
|
26
|
+
# Options: classpath (Array), java, jvm_options (Array), server ('tcp://127.0.0.1:8723'), log_file.
|
|
27
|
+
def configure(**options)
|
|
28
|
+
@lock.synchronize do
|
|
29
|
+
raise BridgeError, 'the PD4ML bridge runs already: configure before the first use' if @default
|
|
30
|
+
@options = @options.merge(options)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The shared bridge, started on first use.
|
|
35
|
+
def get
|
|
36
|
+
@lock.synchronize do
|
|
37
|
+
unless @at_exit
|
|
38
|
+
@at_exit = true
|
|
39
|
+
at_exit { shutdown } # Java ends with the Ruby process, before it exits
|
|
40
|
+
end
|
|
41
|
+
@default ||= new(**@options)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def started?
|
|
46
|
+
@lock.synchronize { !@default.nil? }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Stops the shared bridge (it starts again on the next use).
|
|
50
|
+
def shutdown
|
|
51
|
+
@lock.synchronize do
|
|
52
|
+
@default&.close
|
|
53
|
+
@default = nil
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def forget(bridge)
|
|
58
|
+
@lock.synchronize { @default = nil if @default.equal?(bridge) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The classpath the bridge starts with: configured, from the environment, or the installed build.
|
|
62
|
+
def classpath(options = @options)
|
|
63
|
+
entries = options[:classpath]
|
|
64
|
+
entries ||= ENV['PD4ML_CLASSPATH'].split(File::PATH_SEPARATOR) if ENV['PD4ML_CLASSPATH'].to_s != ''
|
|
65
|
+
entries ||= File.read(ENV['PD4ML_CLASSPATH_FILE']).split(/\r?\n/) if ENV['PD4ML_CLASSPATH_FILE'].to_s != ''
|
|
66
|
+
unless entries
|
|
67
|
+
entries = Installer.installed_classpath
|
|
68
|
+
if entries.empty? && Installer.auto_install?
|
|
69
|
+
warn 'pd4ml: no PD4ML build installed yet: downloading the most recent release ' \
|
|
70
|
+
"(once; 'pd4ml install' updates it, PD4ML_AUTO_INSTALL=0 turns this off)"
|
|
71
|
+
Installer.install
|
|
72
|
+
entries = Installer.installed_classpath
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
entries.map(&:strip).reject(&:empty?).flat_map do |e|
|
|
76
|
+
if e.end_with?('/*', '\\*')
|
|
77
|
+
Dir.glob(File.join(e[0..-3].tr('\\', '/'), '*.jar')).sort
|
|
78
|
+
else
|
|
79
|
+
[e]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The java executable: PD4ML_JAVA, JAVA_HOME/bin/java, else `java`.
|
|
85
|
+
def java(options = @options)
|
|
86
|
+
return options[:java] if options[:java]
|
|
87
|
+
return ENV['PD4ML_JAVA'] if ENV['PD4ML_JAVA'].to_s != ''
|
|
88
|
+
|
|
89
|
+
home = ENV['JAVA_HOME'].to_s
|
|
90
|
+
unless home.empty?
|
|
91
|
+
exe = File.join(home, 'bin', Gem.win_platform? ? 'java.exe' : 'java')
|
|
92
|
+
return exe if File.exist?(exe)
|
|
93
|
+
end
|
|
94
|
+
'java'
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The command line that runs the bridge (with a port: a bridge server).
|
|
98
|
+
def command(options = @options, port: nil)
|
|
99
|
+
cp = classpath(options)
|
|
100
|
+
if cp.empty?
|
|
101
|
+
raise BridgeError, "PD4ML not found: install a build with 'pd4ml install' (or Pd4ml.install), or point at " \
|
|
102
|
+
'one with Pd4ml.configure(classpath: [...]), PD4ML_CLASSPATH or PD4ML_CLASSPATH_FILE'
|
|
103
|
+
end
|
|
104
|
+
args = [java(options), '-Djava.awt.headless=true', *Array(options[:jvm_options])]
|
|
105
|
+
args.concat(ENV['PD4ML_JVM_OPTIONS'].split) if ENV['PD4ML_JVM_OPTIONS'].to_s != ''
|
|
106
|
+
args.push('-cp', [BRIDGE_JAR, *cp].join(File::PATH_SEPARATOR), 'com.pd4ml.bridge.Bridge')
|
|
107
|
+
args.push('--port', port.to_s) if port
|
|
108
|
+
args
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def initialize(**options)
|
|
113
|
+
@lock = Monitor.new
|
|
114
|
+
@next_id = 1
|
|
115
|
+
@next_callback = 1
|
|
116
|
+
@callbacks = {}
|
|
117
|
+
@pending = {}
|
|
118
|
+
@released = Queue.new
|
|
119
|
+
@callback_error = nil
|
|
120
|
+
@closed = false
|
|
121
|
+
server = options[:server] || ENV['PD4ML_BRIDGE']
|
|
122
|
+
if server.to_s != ''
|
|
123
|
+
uri = URI(server)
|
|
124
|
+
begin
|
|
125
|
+
@socket = TCPSocket.new(uri.host, uri.port)
|
|
126
|
+
rescue SystemCallError => e
|
|
127
|
+
raise BridgeError, "cannot connect to the PD4ML bridge at #{server}: #{e.message}"
|
|
128
|
+
end
|
|
129
|
+
@in = @out = @socket
|
|
130
|
+
else
|
|
131
|
+
cmd = self.class.command(options)
|
|
132
|
+
begin
|
|
133
|
+
@in, @out, err, @thread = Open3.popen3(*cmd)
|
|
134
|
+
rescue SystemCallError => e
|
|
135
|
+
raise BridgeError, "cannot start java (#{cmd.first}): #{e.message}"
|
|
136
|
+
end
|
|
137
|
+
log = options[:log_file] || ENV['PD4ML_BRIDGE_LOG']
|
|
138
|
+
@stderr_tail = +''
|
|
139
|
+
# drained either way, so Java never blocks on a full pipe
|
|
140
|
+
@stderr_reader = Thread.new do
|
|
141
|
+
File.open(log, 'a') do |f|
|
|
142
|
+
err.each_line { |l| f.write(l); keep_tail(l) }
|
|
143
|
+
end if log
|
|
144
|
+
err.each_line { |l| keep_tail(l) } unless log
|
|
145
|
+
rescue IOError
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
@in.binmode
|
|
150
|
+
@out.binmode
|
|
151
|
+
@in.sync = true
|
|
152
|
+
hello = request({ 'op' => 'hello' })
|
|
153
|
+
raise BridgeError, 'incompatible PD4ML bridge protocol' unless hello.is_a?(Hash) && hello['protocol'] == 1
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def close
|
|
157
|
+
@lock.synchronize do
|
|
158
|
+
return if @closed
|
|
159
|
+
|
|
160
|
+
@closed = true
|
|
161
|
+
begin
|
|
162
|
+
@in.write("{\"id\":0,\"op\":\"close\"}\n")
|
|
163
|
+
@in.close unless @socket
|
|
164
|
+
rescue IOError, SystemCallError
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
@socket&.close
|
|
168
|
+
# let Java finish (it exits on 'close' or end of input), so it never outlives Ruby
|
|
169
|
+
if @thread && !@thread.join(5)
|
|
170
|
+
begin
|
|
171
|
+
Process.kill(:KILL, @thread.pid)
|
|
172
|
+
rescue SystemCallError
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
175
|
+
@thread.join(5)
|
|
176
|
+
end
|
|
177
|
+
@stderr_reader&.join(1)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# ------------------------------------------------------------------------------- protocol
|
|
182
|
+
|
|
183
|
+
# Sends one request and returns its decoded result; callbacks run while it waits. With `outs`
|
|
184
|
+
# (an Array), the content of Output arguments is stored there (binary Strings).
|
|
185
|
+
def request(req, outs = nil)
|
|
186
|
+
response = @lock.synchronize do
|
|
187
|
+
raise BridgeError, 'the PD4ML bridge is closed' if @closed
|
|
188
|
+
|
|
189
|
+
refs = []
|
|
190
|
+
refs << @released.pop until @released.empty?
|
|
191
|
+
exchange({ 'op' => 'release', 'refs' => refs }) unless refs.empty?
|
|
192
|
+
exchange(req)
|
|
193
|
+
end
|
|
194
|
+
raise JavaError.from(response['error']) if response['error']
|
|
195
|
+
|
|
196
|
+
if outs && response['out'].is_a?(Array)
|
|
197
|
+
response['out'].each_with_index { |data, i| outs[i] = data&.unpack1('m0') }
|
|
198
|
+
end
|
|
199
|
+
if (e = @callback_error)
|
|
200
|
+
@callback_error = nil
|
|
201
|
+
begin
|
|
202
|
+
raise e
|
|
203
|
+
rescue StandardError
|
|
204
|
+
raise CallbackError, "a callback failed: #{e.message}"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
decode(response['result'])
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# ----------------------------------------------------------------------------- operations
|
|
211
|
+
|
|
212
|
+
def new_object(cls, args) = request({ 'op' => 'new', 'class' => cls, 'args' => args })
|
|
213
|
+
|
|
214
|
+
def call(target, method, args, outs = nil)
|
|
215
|
+
request({ 'op' => 'call', 'ref' => handle_of(target), 'method' => method, 'args' => args }, outs)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def call_static(cls, method, args, outs = nil)
|
|
219
|
+
request({ 'op' => 'static', 'class' => cls, 'method' => method, 'args' => args }, outs)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def get_field(target, field)
|
|
223
|
+
if target.is_a?(String)
|
|
224
|
+
request({ 'op' => 'get', 'class' => target, 'field' => field })
|
|
225
|
+
else
|
|
226
|
+
request({ 'op' => 'get', 'ref' => handle_of(target), 'field' => field })
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def set_field(target, field, value)
|
|
231
|
+
if target.is_a?(String)
|
|
232
|
+
request({ 'op' => 'set', 'class' => target, 'field' => field, 'value' => value })
|
|
233
|
+
else
|
|
234
|
+
request({ 'op' => 'set', 'ref' => handle_of(target), 'field' => field, 'value' => value })
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def describe(cls) = request({ 'op' => 'describe', 'class' => cls })
|
|
239
|
+
|
|
240
|
+
def instance_of?(target, cls)
|
|
241
|
+
request({ 'op' => 'instanceof', 'ref' => handle_of(target), 'class' => cls }) == true
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# A Java object implementing `iface`; its methods call the block with (method, args).
|
|
245
|
+
def proxy(iface, &block)
|
|
246
|
+
id = @lock.synchronize { (@next_callback += 1) - 1 }
|
|
247
|
+
@callbacks[id] = block
|
|
248
|
+
request({ 'op' => 'proxy', 'interface' => iface, 'callback' => id })
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Called by JavaObject finalizers; released with the next request.
|
|
252
|
+
def release(handle)
|
|
253
|
+
@released << handle unless @closed
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# ------------------------------------------------------------------------------- values
|
|
257
|
+
|
|
258
|
+
def encode(v)
|
|
259
|
+
case v
|
|
260
|
+
when JavaObject then { '$ref' => v.__handle }
|
|
261
|
+
when Output then { '$out' => v.index }
|
|
262
|
+
when PD4ML then { '$ref' => v.java.__handle }
|
|
263
|
+
when String
|
|
264
|
+
v.encoding == Encoding::BINARY || !v.valid_encoding? ? { '$bytes' => [v].pack('m0') } : v
|
|
265
|
+
when Symbol then v.to_s
|
|
266
|
+
when Pathname, URI::Generic then v.to_s
|
|
267
|
+
when Hash then v.to_h { |k, x| [k.to_s, encode(x)] }
|
|
268
|
+
when Array then v.map { |x| encode(x) }
|
|
269
|
+
when nil, true, false, Integer then v
|
|
270
|
+
when Float
|
|
271
|
+
raise ArgumentError, "not a finite number: #{v}" unless v.finite?
|
|
272
|
+
|
|
273
|
+
v
|
|
274
|
+
else
|
|
275
|
+
if v.respond_to?(:read)
|
|
276
|
+
data = v.read
|
|
277
|
+
{ '$bytes' => [data.to_s].pack('m0') }
|
|
278
|
+
elsif v.respond_to?(:to_str)
|
|
279
|
+
v.to_str
|
|
280
|
+
else
|
|
281
|
+
raise ArgumentError, "cannot pass #{v.class} to Java"
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def decode(v)
|
|
287
|
+
case v
|
|
288
|
+
when Array then v.map { |x| decode(x) }
|
|
289
|
+
when Hash
|
|
290
|
+
if v.key?('$ref')
|
|
291
|
+
JavaObject.new(self, v['$ref'], v['class'], v['enum'])
|
|
292
|
+
elsif v.key?('$bytes')
|
|
293
|
+
v['$bytes'].unpack1('m0')
|
|
294
|
+
else
|
|
295
|
+
v.transform_values { |x| decode(x) }
|
|
296
|
+
end
|
|
297
|
+
else
|
|
298
|
+
v
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
private
|
|
303
|
+
|
|
304
|
+
def keep_tail(line)
|
|
305
|
+
@stderr_tail = (@stderr_tail + line)[-4000..] || (@stderr_tail + line)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def handle_of(target)
|
|
309
|
+
case target
|
|
310
|
+
when JavaObject then target.__handle
|
|
311
|
+
when PD4ML then target.java.__handle
|
|
312
|
+
else raise ArgumentError, "not a Java object: #{target.inspect}"
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def exchange(req)
|
|
317
|
+
id = @next_id
|
|
318
|
+
@next_id += 1
|
|
319
|
+
line = JSON.generate(encode(req.merge('id' => id)))
|
|
320
|
+
begin
|
|
321
|
+
@in.write(line, "\n")
|
|
322
|
+
rescue IOError, SystemCallError => e
|
|
323
|
+
gone("the PD4ML bridge is gone (#{e.message})")
|
|
324
|
+
end
|
|
325
|
+
loop do
|
|
326
|
+
return @pending.delete(id) if @pending.key?(id)
|
|
327
|
+
|
|
328
|
+
raw = @out.gets
|
|
329
|
+
gone('the PD4ML bridge closed the connection') if raw.nil?
|
|
330
|
+
next if raw.strip.empty?
|
|
331
|
+
|
|
332
|
+
msg = JSON.parse(raw.force_encoding(Encoding::UTF_8))
|
|
333
|
+
if msg.key?('callback')
|
|
334
|
+
dispatch(msg)
|
|
335
|
+
elsif msg['id'] == id
|
|
336
|
+
return msg
|
|
337
|
+
else
|
|
338
|
+
@pending[msg['id']] = msg # an outer request's response, during a callback
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def dispatch(msg)
|
|
344
|
+
callback = @callbacks[msg['callback']]
|
|
345
|
+
return if callback.nil? || @callback_error
|
|
346
|
+
|
|
347
|
+
begin
|
|
348
|
+
callback.call(msg['method'], Array(msg['args']).map { |a| decode(a) })
|
|
349
|
+
rescue StandardError => e
|
|
350
|
+
@callback_error = e # raised by the request that caused the callback
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def gone(why)
|
|
355
|
+
@closed = true
|
|
356
|
+
self.class.forget(self)
|
|
357
|
+
detail = @stderr_tail.to_s.strip
|
|
358
|
+
raise BridgeError, why + (detail.empty? ? '' : "\n#{detail}") +
|
|
359
|
+
(ENV['PD4ML_BRIDGE_LOG'] ? '' : ' (PD4ML_BRIDGE_LOG=file keeps its output)')
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
data/lib/pd4ml/errors.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pd4ml
|
|
4
|
+
# The bridge to Java cannot start or broke down (no Java, no PD4ML build, process gone).
|
|
5
|
+
class BridgeError < StandardError; end
|
|
6
|
+
|
|
7
|
+
# A PD4ML build could not be found, downloaded or verified.
|
|
8
|
+
class InstallError < StandardError; end
|
|
9
|
+
|
|
10
|
+
# A Ruby block (a progress listener...) raised while PD4ML ran; the error is #cause.
|
|
11
|
+
class CallbackError < StandardError; end
|
|
12
|
+
|
|
13
|
+
# A Java exception thrown by PD4ML (or any Java code called): its Java class and superclasses,
|
|
14
|
+
# message, causes and stack trace.
|
|
15
|
+
#
|
|
16
|
+
# begin
|
|
17
|
+
# pd.read_html('missing.html')
|
|
18
|
+
# rescue Pd4ml::JavaError => e
|
|
19
|
+
# raise unless e.instance_of_java?('java.io.IOException')
|
|
20
|
+
# end
|
|
21
|
+
class JavaError < StandardError
|
|
22
|
+
attr_reader :java_class, :hierarchy, :java_message, :causes, :java_trace
|
|
23
|
+
|
|
24
|
+
def initialize(java_message, java_class, hierarchy = [], causes = [], java_trace = '')
|
|
25
|
+
super("#{java_class}: #{java_message}")
|
|
26
|
+
@java_message = java_message
|
|
27
|
+
@java_class = java_class
|
|
28
|
+
@hierarchy = hierarchy
|
|
29
|
+
@causes = causes
|
|
30
|
+
@java_trace = java_trace
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Whether the Java exception is of the class or a subclass ('java.io.IOException').
|
|
34
|
+
def instance_of_java?(name)
|
|
35
|
+
@java_class == name || @hierarchy.include?(name)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.from(e)
|
|
39
|
+
new(e['message'].to_s, e['class'] || 'java.lang.Throwable', e['hierarchy'] || [], e['causes'] || [], e['trace'].to_s)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|