open 0.1.30 → 0.2.22

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +110 -31
  3. data/bin/open +1 -1
  4. data/bin/open_in_browser +1 -1
  5. data/doc/README.gen +72 -8
  6. data/doc/deprecated_code/deprecated_code.md +26 -0
  7. data/img/open_logo.png +0 -0
  8. data/lib/open/base/base.rb +265 -128
  9. data/lib/open/books/README.md +8 -0
  10. data/lib/open/books/books.rb +100 -0
  11. data/lib/open/constants/constants.rb +86 -8
  12. data/lib/open/in_browser/in_browser.rb +501 -319
  13. data/lib/open/in_editor/in_editor.rb +367 -162
  14. data/lib/open/last/last.rb +14 -12
  15. data/lib/open/last_url/last_url.rb +4 -3
  16. data/lib/open/nano_open/nano_open.rb +30 -5
  17. data/lib/open/{open.rb → open/open.rb} +596 -654
  18. data/lib/open/project/project.rb +3 -2
  19. data/lib/open/requires/failsafe_require_of_beautiful_url.rb +9 -0
  20. data/lib/open/requires/require_the_project.rb +2 -2
  21. data/lib/open/requires/require_yaml.rb +13 -0
  22. data/lib/open/these_files/these_files.rb +1 -1
  23. data/lib/open/toplevel_methods/browser.rb +71 -0
  24. data/lib/open/toplevel_methods/delay.rb +23 -0
  25. data/lib/open/toplevel_methods/e.rb +14 -0
  26. data/lib/open/toplevel_methods/editor.rb +41 -0
  27. data/lib/open/toplevel_methods/host_os.rb +25 -0
  28. data/lib/open/toplevel_methods/is_on_roebe.rb +16 -0
  29. data/lib/open/toplevel_methods/is_on_windows.rb +26 -0
  30. data/lib/open/toplevel_methods/misc.rb +50 -0
  31. data/lib/open/version/version.rb +2 -2
  32. data/lib/open/with_delay/with_delay.rb +10 -11
  33. data/open.gemspec +3 -3
  34. data/test/testing_open.rb +1 -1
  35. data/test/testing_open_in_browser.rb +16 -0
  36. data/test/testing_open_via_launchy.rb +3 -0
  37. data/test/testing_shortcuts.rb +3 -0
  38. metadata +25 -9
  39. data/lib/open/toplevel_code/toplevel_code.rb +0 -189
@@ -2,64 +2,103 @@
2
2
  # Encoding: UTF-8
3
3
  # frozen_string_literal: true
4
4
  # =========================================================================== #
5
+ # === Open::Base
6
+ #
7
+ # Usage example:
8
+ #
9
+ # Open::Base.new(ARGV)
10
+ #
11
+ # =========================================================================== #
5
12
  # require 'open/base/base.rb'
6
- # < Base
13
+ # < ::Open::Base
7
14
  # =========================================================================== #
8
15
  module Open
9
16
 
10
17
  class Base # === Open::Base
11
18
 
12
- require 'yaml'
19
+ alias e puts
13
20
 
14
21
  require 'fileutils'
15
-
16
- begin
17
- require 'colours'
18
- include Colours::E
19
- rescue LoadError; end
20
-
21
- begin
22
- require 'beautiful_url'
23
- rescue LoadError
24
- # puts 'BeautifulUrl is not available.'
25
- end
22
+ require 'open/requires/require_yaml.rb'
23
+ require 'open/constants/constants.rb'
24
+ require 'open/project/project.rb'
25
+ require 'open/toplevel_methods/delay.rb'
26
+ require 'open/toplevel_methods/misc.rb'
27
+ require 'open/toplevel_methods/host_os.rb'
26
28
 
27
29
  begin
28
30
  require 'opn'
29
31
  rescue LoadError; end
30
32
 
31
33
  begin
32
- require 'convert_global_env'
34
+ require 'roebe/classes/find_expanded_alias.rb'
33
35
  rescue LoadError; end
34
36
 
35
37
  begin
36
- require 'roebe/classes/find_expanded_alias.rb'
38
+ require 'colours/html_colours'
39
+ include Colours::E
37
40
  rescue LoadError; end
38
41
 
39
- require 'open/constants/constants.rb'
40
- require 'open/toplevel_code/toplevel_code.rb'
41
-
42
42
  # ========================================================================= #
43
43
  # === NAMESPACE
44
44
  # ========================================================================= #
45
45
  NAMESPACE = inspect
46
46
 
47
47
  # ========================================================================= #
48
- # === rev
48
+ # === initialize
49
49
  # ========================================================================= #
50
- def rev
51
- if Object.const_defined? :Colours
52
- ::Colours.rev
53
- else
54
- ''
55
- end
50
+ def initialize(i = ARGV)
51
+ set_commandline_arguments(i)
52
+ end
53
+
54
+ # ========================================================================= #
55
+ # === reset (reset tag)
56
+ # ========================================================================= #
57
+ def reset
58
+ # ======================================================================= #
59
+ # === @namespace
60
+ # ======================================================================= #
61
+ @namespace = NAMESPACE
62
+ # ======================================================================= #
63
+ # === @be_verbose
64
+ # ======================================================================= #
65
+ @be_verbose = true
66
+ end
67
+
68
+ require 'open/toplevel_methods/is_on_windows.rb'
69
+ # ========================================================================= #
70
+ # === is_on_windows?
71
+ # ========================================================================= #
72
+ def is_on_windows?(
73
+ i = ::Open.host_os?
74
+ )
75
+ ::Open.is_on_windows?(i)
76
+ end; alias on_windows? is_on_windows? # === on_windows?
77
+
78
+ # ========================================================================= #
79
+ # === return_pwd
80
+ # ========================================================================= #
81
+ def return_pwd
82
+ "#{Dir.pwd}/".squeeze('/')
83
+ end
84
+
85
+ # ========================================================================= #
86
+ # == snakecase
87
+ # ========================================================================= #
88
+ def snakecase(i)
89
+ i.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
90
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
91
+ tr('-', '_').
92
+ gsub(/\s/, '_').
93
+ gsub(/__+/, '_').
94
+ downcase
56
95
  end
57
96
 
58
97
  # ========================================================================= #
59
98
  # === simp
60
99
  # ========================================================================= #
61
100
  def simp(i = '')
62
- if Object.const_defined? :Colours
101
+ if Object.const_defined?(:Colours) and ::Colours.respond_to?(:simp)
63
102
  return ::Colours.simp(i)
64
103
  else
65
104
  return i
@@ -70,7 +109,7 @@ class Base # === Open::Base
70
109
  # === sfancy
71
110
  # ========================================================================= #
72
111
  def sfancy(i = '')
73
- if Object.const_defined? :Colours
112
+ if Object.const_defined?(:Colours) and ::Colours.respond_to?(:sfancy)
74
113
  return ::Colours.sfancy(i)
75
114
  else
76
115
  return i
@@ -81,7 +120,7 @@ class Base # === Open::Base
81
120
  # === steelblue
82
121
  # ========================================================================= #
83
122
  def steelblue(i = '')
84
- if Object.const_defined? :Colours
123
+ if Object.const_defined?(:Colours) and ::Colours.respond_to?(:steelblue)
85
124
  return ::Colours.steelblue(i)
86
125
  else
87
126
  return i
@@ -110,7 +149,7 @@ class Base # === Open::Base
110
149
  # === sfile
111
150
  # ========================================================================= #
112
151
  def sfile(i = '')
113
- if Object.const_defined? :Colours
152
+ if Object.const_defined?(:Colours) and ::Colours.respond_to?(:sfile)
114
153
  return ::Colours.sfile(i)
115
154
  else
116
155
  return i
@@ -125,78 +164,56 @@ class Base # === Open::Base
125
164
  end
126
165
 
127
166
  # ========================================================================= #
128
- # === esystem
129
- #
130
- # This is a bit of an ad-hoc fix to make this work on windows as well.
167
+ # === host_os?
131
168
  #
132
- # Eventually may have to rewrite the method a little bit, but for now
133
- # (September 2021) this has to suffice.
169
+ # Return the host-operating system via this method.
134
170
  # ========================================================================= #
135
- def esystem(
136
- i, do_show_the_command_that_will_be_used = true
137
- )
138
- if is_on_windows?
139
- # i = i.to_s.sub(/\\/,'\\') if i.include?("\\")
140
- i = '"'+i.to_s+'"' if i.include?(' ')
141
- end
142
- e i if do_show_the_command_that_will_be_used
143
- system(i)
171
+ def host_os?
172
+ ::Open.host_os?
144
173
  end
145
174
 
175
+ require 'open/toplevel_methods/is_on_roebe.rb'
146
176
  # ========================================================================= #
147
- # === sanitize
177
+ # === is_on_roebe?
148
178
  # ========================================================================= #
149
- def sanitize(i)
150
- return i unless Object.const_defined? :ConvertGlobalEnv
151
- begin
152
- return ConvertGlobalEnv.convert(i)
153
- rescue NoMethodError
154
- i
155
- end
179
+ def is_on_roebe?
180
+ ::Open.is_on_roebe?
156
181
  end
157
182
 
158
183
  # ========================================================================= #
159
- # === return_pwd
184
+ # === namespace?
160
185
  # ========================================================================= #
161
- def return_pwd
162
- "#{Dir.pwd}/".squeeze('/')
186
+ def namespace?
187
+ @namespace
163
188
  end
164
189
 
165
190
  # ========================================================================= #
166
- # == snakecase
191
+ # === ecomment
167
192
  # ========================================================================= #
168
- def snakecase(i)
169
- i.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
170
- gsub(/([a-z\d])([A-Z])/,'\1_\2').
171
- tr('-', '_').
172
- gsub(/\s/, '_').
173
- gsub(/__+/, '_').
174
- downcase
193
+ def ecomment(i = '')
194
+ ::Colours.ecomment(i)
175
195
  end
176
196
 
197
+ require 'open/toplevel_methods/browser.rb'
177
198
  # ========================================================================= #
178
- # === reset (reset tag)
199
+ # === use_which_browser?
179
200
  # ========================================================================= #
180
- def reset
181
- # ======================================================================= #
182
- # === @namespace
183
- # ======================================================================= #
184
- @namespace = NAMESPACE
185
- # ======================================================================= #
186
- # === @be_verbose
187
- # ======================================================================= #
188
- @be_verbose = true
201
+ def use_which_browser?
202
+ ::Open.use_which_browser?
189
203
  end
190
204
 
191
205
  # ========================================================================= #
192
- # === opnn
206
+ # === mkdir (mkdir tag)
193
207
  # ========================================================================= #
194
- def opnn(
195
- i = { namespace: @namespace }
196
- )
197
- if Object.const_defined? :Opn
198
- Opn.opn(i)
199
- end
208
+ def mkdir(i)
209
+ FileUtils.mkdir_p(i)
210
+ end
211
+
212
+ # ========================================================================= #
213
+ # === touch (touch tag)
214
+ # ========================================================================= #
215
+ def touch(i)
216
+ FileUtils.touch(i)
200
217
  end
201
218
 
202
219
  # ========================================================================= #
@@ -208,6 +225,22 @@ class Base # === Open::Base
208
225
  @commandline_arguments = [i].flatten.compact
209
226
  end
210
227
 
228
+ # ========================================================================= #
229
+ # === commandline_arguments?
230
+ # ========================================================================= #
231
+ def commandline_arguments?
232
+ @commandline_arguments
233
+ end
234
+
235
+ # ========================================================================= #
236
+ # === commandline_arguments_without_hyphens
237
+ # ========================================================================= #
238
+ def commandline_arguments_without_hyphens
239
+ @commandline_arguments.reject {|entry|
240
+ entry.start_with?('--')
241
+ }
242
+ end
243
+
211
244
  # ========================================================================= #
212
245
  # === first_argument?
213
246
  # ========================================================================= #
@@ -215,94 +248,158 @@ class Base # === Open::Base
215
248
  @commandline_arguments.first
216
249
  end
217
250
 
251
+ require 'open/toplevel_methods/editor.rb'
218
252
  # ========================================================================= #
219
- # === rds
253
+ # === use_which_editor?
220
254
  # ========================================================================= #
221
- def rds(i)
222
- i.squeeze('/')
255
+ def use_which_editor?
256
+ ::Open.use_which_editor?
223
257
  end
224
258
 
225
259
  # ========================================================================= #
226
- # === mkdir
260
+ # === opnn
227
261
  # ========================================================================= #
228
- def mkdir(i)
229
- FileUtils.mkdir_p(i)
262
+ def opnn(
263
+ i = {
264
+ namespace: @namespace
265
+ }
266
+ )
267
+ if Object.const_defined? :Opn
268
+ Opn.opn(i)
269
+ end
230
270
  end
231
271
 
232
272
  # ========================================================================= #
233
- # === touch
273
+ # === use_which_delay?
234
274
  # ========================================================================= #
235
- def touch(i)
236
- FileUtils.touch(i)
275
+ def use_which_delay?
276
+ Open.use_which_delay?.to_f
237
277
  end
238
278
 
239
279
  # ========================================================================= #
240
- # === use_which_editor?
280
+ # === opne
241
281
  # ========================================================================= #
242
- def use_which_editor?
243
- ::Open.use_which_editor?
282
+ def opne(i = '')
283
+ opn(namespace: @namespace); e i
244
284
  end
245
285
 
286
+ begin
287
+ require 'beautiful_url'
288
+ rescue LoadError; end
246
289
  # ========================================================================= #
247
- # === use_which_browser?
290
+ # === beautiful_url
291
+ #
292
+ # Wrapper-method towards BeautifulUrl.
248
293
  # ========================================================================= #
249
- def use_which_browser?
250
- ::Open.use_which_browser?
294
+ def beautiful_url(i)
295
+ if beautiful_url_is_available?
296
+ i = BeautifulUrl::BeautifulUrl.new(i).results?
297
+ end
298
+ return i
251
299
  end
252
300
 
253
301
  # ========================================================================= #
254
- # === this_file_was_not_found
255
- #
256
- # Use this whenever we did not find any file.
302
+ # === beautiful_url_is_available?
257
303
  # ========================================================================= #
258
- def this_file_was_not_found(i)
259
- e "No file called `#{sfile(i)}` was found. It "\
260
- "is assumed that it does not exist."
261
- end; alias no_file_exists_at this_file_was_not_found # === no_file_exists_at
262
- alias no_file_was_found_at this_file_was_not_found # === no_file_was_found_at
304
+ def beautiful_url_is_available?
305
+ Object.const_defined? :BeautifulUrl
306
+ end; alias is_beautiful_url_available? beautiful_url_is_available? # === is_beautiful_url_available?
263
307
 
264
308
  # ========================================================================= #
265
- # === host_os?
266
- #
267
- # Return the host-operating system via this method.
309
+ # === try_to_require_the_beautiful_url_project
268
310
  # ========================================================================= #
269
- def host_os?
270
- ::Open.host_os?
311
+ def try_to_require_the_beautiful_url_project
312
+ unless Object.const_defined? :BeautifulUrl
313
+ begin
314
+ require 'open/requires/failsafe_require_of_beautiful_url.rb'
315
+ rescue LoadError; end
316
+ end
271
317
  end
272
318
 
273
319
  # ========================================================================= #
274
- # === is_on_windows?
320
+ # === file_load_default_browser?
275
321
  # ========================================================================= #
276
- def is_on_windows?(i = host_os?)
277
- ::Open.is_on_windows?(i)
278
- end; alias on_windows? is_on_windows? # === on_windows?
322
+ def file_load_default_browser?
323
+ # ======================================================================= #
324
+ # The .yml file will hold which browser is to be used by default.
325
+ # ======================================================================= #
326
+ _ = "#{::Open.project_base_directory?}yaml/use_this_browser.yml"
327
+ if File.exist? _
328
+ return YAML.load_file(_)
329
+ else
330
+ return 'chrome'
331
+ end
332
+ end
279
333
 
280
334
  # ========================================================================= #
281
- # === is_on_roebe?
335
+ # === file_load_default_editor?
282
336
  # ========================================================================= #
283
- def is_on_roebe?
284
- ENV['IS_ROEBE'].to_s == '1'
337
+ def file_load_default_editor?
338
+ # ======================================================================= #
339
+ # The .yml file will hold which editor is to be used by default.
340
+ # ======================================================================= #
341
+ _ = "#{::Open.project_base_directory?}yaml/use_this_editor.yml"
342
+ if File.exist? _
343
+ return YAML.load_file(_)
344
+ else
345
+ return 'bluefish'
346
+ end
285
347
  end
286
348
 
349
+ begin
350
+ require 'convert_global_env'
351
+ rescue LoadError; end
287
352
  # ========================================================================= #
288
- # === ecomment
353
+ # === sanitize_global_environment
354
+ #
355
+ # This method bundles together functionality that ConvertGlobalEnv
356
+ # can handle.
289
357
  # ========================================================================= #
290
- def ecomment(i = '')
291
- ::Colours.ecomment(i)
292
- end
358
+ def sanitize_global_environment(i)
359
+ return i unless Object.const_defined? :ConvertGlobalEnv
360
+ begin
361
+ return ConvertGlobalEnv.convert(i)
362
+ rescue NoMethodError
363
+ return i
364
+ end
365
+ end; alias sanitize sanitize_global_environment # === sanitize
293
366
 
294
367
  # ========================================================================= #
295
- # === beautiful_url
368
+ # === consider_padding
296
369
  #
297
- # Wrapper-method towards BeautifulUrl.
370
+ # Simply add a surrounding '"' to the given input, so that any resulting
371
+ # system() call will still work if we have awkward filenames.
298
372
  # ========================================================================= #
299
- def beautiful_url(i)
300
- if Object.const_defined? :BeautifulUrl
301
- i = BeautifulUrl::BeautifulUrl.new(i).results?
373
+ def consider_padding(i)
374
+ if i.include?(' ') or i.include?('(')
375
+ i = '"'+i+'"'
302
376
  end
303
- return i
377
+ return i # Always return the input, no matter if modified or not.
378
+ end
379
+
380
+ # ========================================================================= #
381
+ # === rds
382
+ # ========================================================================= #
383
+ def rds(i)
384
+ i.squeeze('/')
385
+ end
386
+
387
+ # ========================================================================= #
388
+ # === append_array_to_commandline_arguments
389
+ # ========================================================================= #
390
+ def append_array_to_commandline_arguments(i)
391
+ i = [i].flatten.compact
392
+ @commandline_arguments << i
393
+ @commandline_arguments.flatten!
304
394
  end
305
395
 
396
+ # ========================================================================= #
397
+ # === create_the_internal_hash
398
+ # ========================================================================= #
399
+ def create_the_internal_hash
400
+ @internal_hash = {}
401
+ end; alias reset_the_internal_hash create_the_internal_hash # === reset_the_internal_hash
402
+
306
403
  # ========================================================================= #
307
404
  # === infer_the_namespace
308
405
  #
@@ -320,10 +417,50 @@ class Base # === Open::Base
320
417
  end
321
418
 
322
419
  # ========================================================================= #
323
- # === namespace?
420
+ # === this_file_was_not_found
421
+ #
422
+ # Use this to notify the user, whenever the open-gem was unable to
423
+ # find a file that was assumed to exist locally.
324
424
  # ========================================================================= #
325
- def namespace?
326
- @namespace
425
+ def this_file_was_not_found(i)
426
+ e "#{rev}No file called `#{sfile(i)}#{rev}` was found. It "\
427
+ "is assumed to not exist."
428
+ end; alias no_file_exists_at this_file_was_not_found # === no_file_exists_at
429
+ alias no_file_was_found_at this_file_was_not_found # === no_file_was_found_at
430
+
431
+ # ========================================================================= #
432
+ # === esystem (esystem tag)
433
+ #
434
+ # This method contains a bit of an ad-hoc fix to make this work on
435
+ # windows as well.
436
+ #
437
+ # Eventually may have to rewrite the method a little bit, but for now
438
+ # (September 2021) this has to suffice.
439
+ # ========================================================================= #
440
+ def esystem(
441
+ i,
442
+ do_show_the_command_that_will_be_used = true # This is the default.
443
+ )
444
+ if is_on_windows?
445
+ # i = i.to_s.sub(/\\/,'\\') if i.include?("\\")
446
+ i = '"'+i.to_s+'"' if i.include?(' ')
447
+ end
448
+ e "#{rev}#{i}" if do_show_the_command_that_will_be_used
449
+ system(i)
450
+ end
451
+
452
+ # ========================================================================= #
453
+ # === rev
454
+ # ========================================================================= #
455
+ def rev
456
+ return ::Colours.rev if Object.const_defined? :Colours
457
+ return ''
327
458
  end
328
459
 
329
- end; end
460
+ end; end
461
+
462
+ if __FILE__ == $PROGRAM_NAME
463
+ alias e puts
464
+ base = Open::Base.new(ARGV)
465
+ e base.host_os?
466
+ end # base.rb
@@ -0,0 +1,8 @@
1
+ This directory is specifically to enable the functionality of
2
+ "opening books". Books in this context refers to PDF files,
3
+ that is, .pdf files. In order for this functionality to work,
4
+ you need to have .pdf files available locally, and you also
5
+ need to supply a custom file that keeps track of the themes
6
+ of the books, such as "all books that handle the theme
7
+ genetics". Then, once such a file has been made available,
8
+ class Open::Books will simply open all these books.
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/ruby -w
2
+ # Encoding: UTF-8
3
+ # frozen_string_literal: true
4
+ # =========================================================================== #
5
+ # === Open::Book
6
+ #
7
+ # This class will specifically open "books", that is, .pdf files.
8
+ #
9
+ # Usage example:
10
+ #
11
+ # Open::Book.new(ARGV)
12
+ #
13
+ # =========================================================================== #
14
+ require 'open/base/base.rb'
15
+
16
+ module Open
17
+
18
+ class Book < ::Open::Base # === Open::Book
19
+
20
+ # ========================================================================= #
21
+ # === FILE_TOPICS_PER_BOOK
22
+ # ========================================================================= #
23
+ FILE_TOPICS_PER_BOOK = '/home/x/books/topics_per_book.yml'
24
+
25
+ # ========================================================================= #
26
+ # === initialize
27
+ # ========================================================================= #
28
+ def initialize(
29
+ i = ARGV,
30
+ run_already = true
31
+ )
32
+ reset
33
+ set_commandline_arguments(i)
34
+ run if run_already
35
+ end
36
+
37
+ # ========================================================================= #
38
+ # === reset (reset tag)
39
+ # ========================================================================= #
40
+ def reset
41
+ super()
42
+ infer_the_namespace
43
+ # ======================================================================= #
44
+ # === @dataset
45
+ # ======================================================================= #
46
+ @dataset = nil
47
+ if File.exist?(FILE_TOPICS_PER_BOOK)
48
+ @dataset = YAML.load_file(FILE_TOPICS_PER_BOOK)
49
+ end
50
+ end
51
+
52
+ # ========================================================================= #
53
+ # === main_file?
54
+ # ========================================================================= #
55
+ def main_file?
56
+ FILE_TOPICS_PER_BOOK
57
+ end
58
+
59
+ # ========================================================================= #
60
+ # === open_these_files
61
+ # ========================================================================= #
62
+ def open_these_files(array)
63
+ require 'open/open/open.rb'
64
+ array.map! {|entry|
65
+ entry = entry.dup
66
+ unless entry.include?('/home/x/books/')
67
+ entry.prepend(
68
+ entry.delete_suffix('.pdf')+'/'
69
+ )
70
+ entry.prepend('/home/x/books/')
71
+ end
72
+ entry
73
+ }
74
+ ::Open::Open.new(array)
75
+ end
76
+
77
+ # ========================================================================= #
78
+ # === run (run tag)
79
+ # ========================================================================= #
80
+ def run
81
+ _ = @dataset
82
+ commandline_arguments?.each {|this_topic|
83
+ this_topic = this_topic.dup if this_topic.frozen?
84
+ this_topic.delete!(':') if this_topic.include? ':'
85
+ this_topic.downcase! # Keep it downcased at all times past this point.
86
+ if _.has_key?(this_topic)
87
+ opne 'Processing the topic '+steelblue(this_topic)+' next.'
88
+ open_these_files(_[this_topic])
89
+ else
90
+ opne 'The topic '+steelblue(this_topic)+' was not '\
91
+ 'found in the file '+main_file?+'.'
92
+ end
93
+ }
94
+ end
95
+
96
+ end; end
97
+
98
+ if __FILE__ == $PROGRAM_NAME
99
+ Open::Book.new(ARGV)
100
+ end # openbooks :java