forked from tail-f-systems/JNC
-
Notifications
You must be signed in to change notification settings - Fork 0
/
jnc.py
3068 lines (2645 loc) · 127 KB
/
jnc.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/python
# -*- coding: latin-1 -*-
"""JNC: Java NETCONF Client plug-in
Copyright 2012 Tail-f Systems AB
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
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.
For complete functionality, invoke with:
> pyang \
--path <yang search path> \
--format jnc \
--jnc-output <package.name> \
--jnc-verbose \
--jnc-ignore-errors \
--jnc-import-on-demand \
<file.yang>
Or, if you like to keep things simple:
> pyang -f jnc -d <package.name> <file.yang>
"""
import optparse
import os
import errno
import sys
import collections
import re
from datetime import date
from pyang import plugin, util, error
def pyang_plugin_init():
"""Registers an instance of the jnc plugin"""
plugin.register_plugin(JNCPlugin())
class JNCPlugin(plugin.PyangPlugin):
"""The plug-in class of JNC.
The methods of this class are invoked by pyang during initialization. The
emit method is of particular interest if you are new to writing plugins to
pyang. It is from there that the parsing of the YANG statement tree
emanates, producing the generated classes that constitutes the output of
this plug-in.
"""
def __init__(self):
self.done = set([]) # Helps avoiding processing modules more than once
def add_output_format(self, fmts):
"""Adds 'jnc' as a valid output format and sets the format to jnc if
the -d/--jnc-output option is set, but -f/--format is not.
"""
self.multiple_modules = False
fmts['jnc'] = self
args = sys.argv[1:]
if not any(x in args for x in ('-f', '--format')):
if any(x in args for x in ('-d', '--jnc-output')):
sys.argv.insert(1, '--format')
sys.argv.insert(2, 'jnc')
def add_opts(self, optparser):
"""Adds options to pyang, displayed in the pyang CLI help message"""
optlist = [
optparse.make_option(
'-d', '--jnc-output',
dest='directory',
help='Generate output to DIRECTORY.'),
optparse.make_option(
'--jnc-help',
dest='jnc_help',
action='store_true',
help='Print help on usage of the JNC plugin and exit'),
optparse.make_option(
'--jnc-serial',
dest='serial',
action='store_true',
help='Turn off usage of multiple threads.'),
optparse.make_option(
'--jnc-verbose',
dest='verbose',
action='store_true',
help='Verbose mode: Print detailed debug messages.'),
optparse.make_option(
'--jnc-debug',
dest='debug',
action='store_true',
help='Print debug messages. Redundant if verbose mode is on.'),
optparse.make_option(
'--jnc-no-classes',
dest='no_classes',
action='store_true',
help='Do not generate classes.'),
optparse.make_option(
'--jnc-no-schema',
dest='no_schema',
action='store_true',
help='Do not generate schema.'),
optparse.make_option(
'--jnc-no-pkginfo',
dest='no_pkginfo',
action='store_true',
help='Do not generate package-info files.'),
optparse.make_option(
'--jnc-ignore-errors',
dest='ignore',
action='store_true',
help='Ignore errors from validation.'),
optparse.make_option(
'--jnc-import-on-demand',
dest='import_on_demand',
action='store_true',
help='Use non explicit imports where possible.'),
optparse.make_option(
'--jnc-classpath-schema-loading',
dest='classpath_schema_loading',
action='store_true',
help='Load schema files using classpath rather than location.')
]
g = optparser.add_option_group('JNC output specific options')
g.add_options(optlist)
def setup_ctx(self, ctx):
"""Called after ctx has been set up in main module. Checks if the
jnc help option was supplied and if not, that the -d or
--java-package was used.
ctx -- Context object as defined in __init__.py
"""
if ctx.opts.jnc_help:
self.print_help()
sys.exit(0)
if ctx.opts.format == 'jnc':
if not ctx.opts.directory:
ctx.opts.directory = 'src/gen'
print_warning(msg=('Option -d (or --java-package) not set, ' +
'defaulting to "src/gen".'))
elif 'src' not in ctx.opts.directory:
ctx.opts.directory = 'src/gen'
print_warning(msg=('No "src" in output directory path, ' +
'defaulting to "src/gen".'))
ctx.rootpkg = ctx.opts.directory.rpartition('src')[2][1:]
self.ctx = ctx
self.d = ctx.opts.directory.split('.')
def setup_fmt(self, ctx):
"""Disables implicit errors for the Context"""
ctx.implicit_errors = False
def emit(self, ctx, modules, fd):
"""Generates Java classes from the YANG module supplied to pyang.
The generated classes are placed in the directory specified by the '-d'
or '--java-package' flag, or in "gen" if no such flag was provided,
using the 'directory' attribute of ctx. If there are existing files
in the output directory with the same name as the generated classes,
they are silently overwritten.
ctx -- Context used to get output directory, verbosity mode, error
handling policy (the ignore attribute) and whether or not a
schema file should be generated.
modules -- A list containing a module statement parsed from the YANG
module supplied to pyang.
fd -- File descriptor (ignored).
"""
if ctx.opts.debug or ctx.opts.verbose:
print('JNC plugin starting')
if not ctx.opts.ignore:
for (epos, etag, _) in ctx.errors:
if (error.is_error(error.err_level(etag)) and
etag in ('MODULE_NOT_FOUND', 'MODULE_NOT_FOUND_REV')):
self.fatal("%s contains errors" % epos.top.arg)
if (etag in ('TYPE_NOT_FOUND', 'FEATURE_NOT_FOUND',
'IDENTITY_NOT_FOUND', 'GROUPING_NOT_FOUND')):
print_warning(msg=(etag.lower() + ', generated class ' +
'hierarchy might be incomplete.'), key=etag)
else:
print_warning(msg=(etag.lower() + ', aborting.'), key=etag)
self.fatal("%s contains errors" % epos.top.arg)
# Sweep, adding included and imported modules, until no change
module_set = set(modules)
num_modules = 0
while num_modules != len(module_set):
num_modules = len(module_set)
for module in list(module_set):
imported = map(lambda x: x.arg, search(module, 'import'))
included = map(lambda x: x.arg, search(module, 'include'))
for (module_stmt, rev) in self.ctx.modules:
if module_stmt in (imported + included):
module_set.add(self.ctx.modules[(module_stmt, rev)])
# Generate files from main modules
for module in filter(lambda s: s.keyword == 'module', module_set):
self.generate_from(module)
# Generate files from augmented modules
for aug_module in augmented_modules.values():
self.generate_from(aug_module)
# Print debug messages saying that we're done.
if ctx.opts.debug or ctx.opts.verbose:
if not self.ctx.opts.no_classes:
print('Java classes generation COMPLETE.')
if not self.ctx.opts.no_schema:
print('Schema generation COMPLETE.')
def generate_from(self, module):
"""Generates classes, schema file and pkginfo files for module,
according to options set in self.ctx. The attributes self.directory
and self.d are used to determine where to generate the files.
module -- Module statement to generate files from
"""
if module in self.done:
return
self.done.add(module)
subpkg = camelize(module.arg)
if self.ctx.rootpkg:
fullpkg = '.'.join([self.ctx.rootpkg, subpkg])
else:
fullpkg = subpkg
d = os.sep.join(self.d + [subpkg])
if not self.ctx.opts.no_classes:
# Generate Java classes
src = ('module "' + module.arg + '", revision: "' +
util.get_latest_revision(module) + '".')
generator = ClassGenerator(module,
path=os.sep.join([self.ctx.opts.directory, subpkg]),
package=fullpkg, src=src, ctx=self.ctx)
generator.generate()
if not self.ctx.opts.no_schema:
# Generate external schema
schema_nodes = ['<schema>']
stmts = search(module, node_stmts)
module_root = SchemaNode(module, '/')
schema_nodes.extend(module_root.as_list())
if self.ctx.opts.verbose:
print('Generating schema node "/"...')
schema_generator = SchemaGenerator(stmts, '/', self.ctx)
schema_nodes.extend(schema_generator.schema_nodes())
for i in range(1, len(schema_nodes)):
# Indent all but the first and last line
if schema_nodes[i] in ('<node>', '</node>'):
schema_nodes[i] = ' ' * 4 + schema_nodes[i]
else:
schema_nodes[i] = ' ' * 8 + schema_nodes[i]
schema_nodes.append('</schema>')
name = normalize(search_one(module, 'prefix').arg)
write_file(d, name + '.schema', '\n'.join(schema_nodes), self.ctx)
if not self.ctx.opts.no_pkginfo:
# Generate package-info.java for javadoc
pkginfo_generator = PackageInfoGenerator(d, module, self.ctx)
pkginfo_generator.generate_package_info()
if self.ctx.opts.debug or self.ctx.opts.verbose:
print('pkg ' + fullpkg + ' generated')
def fatal(self, exitCode=1):
"""Raise an EmitError"""
raise error.EmitError(self, exitCode)
def print_help(self):
"""Prints a description of what JNC is and how to use it"""
print('''
The JNC (Java NETCONF Client) plug-in can be used to generate a Java class
hierarchy from a single YANG data model. Together with the JNC library, these
generated Java classes may be used as the foundation for a NETCONF client
(AKA manager) written in Java.
The different types of generated files are:
Root class -- This class has the name of the prefix of the YANG module, and
contains fields with the prefix and namespace as well as methods
that enables the JNC library to use the other generated classes
when interacting with a NETCONF server.
YangElement -- Each YangElement corresponds to a container, list or
notification in the YANG model. They represent tree nodes of a
configuration and provides methods to modify the configuration
in accordance with the YANG model that they were generated from.
The top-level nodes in the YANG model will have their
corresponding YangElement classes generated in the output
directory together with the root class. Their respective
subnodes are generated in subpackages with names corresponding
to the name of the parent.
YangTypes -- For each derived type in the YANG model, a class is generated to
the root of the output directory. The derived type may either
extend another derived type class, or the JNC type class
corresponding to a built-in YANG type.
Packageinfo -- For each package in the generated Java class hierarchy, a
package-info.java file is generated, which can be useful when
generating javadoc for the hierarchy.
Schema file -- If enabled, an XML file containing structured information about
the generated Java classes is generated. It contains tagpaths,
namespace, primitive-type and other useful meta-information.
The typical use case for these classes is as part of a JAVA network management
system (EMS), to enable retrieval and/or storing of configurations on NETCONF
agents/servers with specific capabilities.
One way to use the JNC plug-in of pyang is
$ pyang -f jnc --jnc-output package.dir <file.yang>
Type '$ pyang --help' for more details on how to use pyang.
''')
com_tailf_jnc = {'Attribute', 'Capabilities', 'ConfDSession',
'DefaultIOSubscriber', 'Device', 'DeviceUser', 'DummyElement',
'Element', 'ElementChildrenIterator', 'ElementHandler',
'ElementLeafListValueIterator', 'IOSubscriber',
'JNCException', 'Leaf', 'NetconfSession', 'NodeSet', 'Path',
'PathCreate', 'Prefix', 'PrefixMap', 'RevisionInfo',
'RpcError', 'SchemaNode', 'SchemaParser', 'SchemaTree',
'SSHConnection', 'SSHSession', 'Tagpath', 'TCPConnection',
'TCPSession', 'Transport', 'Utils', 'XMLParser',
'YangBaseInt', 'YangBaseString', 'YangBaseType', 'YangBinary',
'YangBits', 'YangBoolean', 'YangDecimal64', 'YangElement',
'YangEmpty', 'YangEnumeration', 'YangException',
'YangIdentityref', 'YangInt16', 'YangInt32', 'YangInt64',
'YangInt8', 'YangLeafref', 'YangString', 'YangType',
'YangUInt16', 'YangUInt32', 'YangUInt64', 'YangUInt8',
'YangUnion', 'YangXMLParser'}
java_reserved_words = {'abstract', 'assert', 'boolean', 'break', 'byte',
'case', 'catch', 'char', 'class', 'const', 'continue',
'default', 'double', 'do', 'else', 'enum', 'extends',
'false','final', 'finally', 'float', 'for', 'goto',
'if', 'implements', 'import', 'instanceof', 'int',
'interface', 'long', 'native', 'new', 'null', 'package',
'private', 'protected', 'public', 'return', 'short',
'static', 'strictfp', 'super', 'switch', 'synchronized',
'this', 'throw', 'throws', 'transient', 'true', 'try',
'void', 'volatile', 'while'}
"""A set of all identifiers that are reserved in Java"""
java_literals = {'true', 'false', 'null'}
"""The boolean and null literals of Java"""
java_lang = {'Appendable', 'CharSequence', 'Cloneable', 'Comparable',
'Iterable', 'Readable', 'Runnable', 'Boolean', 'Byte',
'Character', 'Class', 'ClassLoader', 'Compiler', 'Double',
'Enum', 'Float', 'Integer', 'Long', 'Math', 'Number',
'Object', 'Package', 'Process', 'ProcessBuilder',
'Runtime', 'RuntimePermission', 'SecurityManager',
'Short', 'StackTraceElement', 'StrictMath', 'String',
'StringBuffer', 'StringBuilder', 'System', 'Thread',
'ThreadGroup', 'ThreadLocal', 'Throwable', 'Void'}
"""A subset of the java.lang classes"""
java_util = {'Collection', 'Enumeration', 'Iterator', 'List', 'ListIterator',
'Map', 'Queue', 'Set', 'ArrayList', 'Arrays', 'HashMap',
'HashSet', 'Hashtable', 'LinkedList', 'Properties', 'Random',
'Scanner', 'Stack', 'StringTokenizer', 'Timer', 'TreeMap',
'TreeSet', 'UUID', 'Vector'}
"""A subset of the java.util interfaces and classes"""
java_built_in = java_reserved_words | java_literals | java_lang
"""Identifiers that shouldn't be imported in Java"""
yangelement_stmts = {'container', 'list', 'notification'}
"""Keywords of statements that YangElement classes are generated from"""
leaf_stmts = {'leaf', 'leaf-list'}
"""Leaf and leaf-list statement keywords"""
module_stmts = {'module', 'submodule'}
"""Module and submodule statement keywords"""
node_stmts = module_stmts | yangelement_stmts | leaf_stmts
"""Keywords of statements that make up a configuration tree"""
package_info = '''/**
* This class hierarchy was generated from the Yang module{0}
* by the <a target="_top" href="https://github.com/tail-f-systems/JNC">JNC</a> plugin of <a target="_top" href="http://code.google.com/p/pyang/">pyang</a>.
* The generated classes may be used to manipulate pieces of configuration data
* with NETCONF operations such as edit-config, delete-config and lock. These
* operations are typically accessed through the JNC Java library by
* instantiating Device objects and setting up NETCONF sessions with real
* devices using a compatible YANG model.
* <p>{1}
* @see <a target="_top" href="https://github.com/tail-f-systems/JNC">JNC project page</a>
* @see <a target="_top" href="ftp://ftp.rfc-editor.org/in-notes/rfc6020.txt">RFC 6020: YANG - A Data Modeling Language for the Network Configuration Protocol (NETCONF)</a>
* @see <a target="_top" href="ftp://ftp.rfc-editor.org/in-notes/rfc6241.txt">RFC 6241: Network Configuration Protocol (NETCONF)</a>
* @see <a target="_top" href="ftp://ftp.rfc-editor.org/in-notes/rfc6242.txt">RFC 6242: Using the NETCONF Protocol over Secure Shell (SSH)</a>
* @see <a target="_top" href="http://www.tail-f.com">Tail-f Systems</a>
*/
package '''
"""Format string used in package-info files"""
outputted_warnings = []
"""A list of warning message IDs that are used to avoid duplicate warnings"""
augmented_modules = {}
"""A dict of external modules that are augmented by the YANG module"""
camelized_stmt_args = {}
"""Cache containing camelized versions of statement identifiers"""
normalized_stmt_args = {}
"""Cache containing normalized versions of statement identifiers"""
class_hierarchy = {}
"""Dict that map package names to sets of names of classes to be generated"""
def print_warning(msg='', key='', ctx=None):
"""Prints msg to stderr if ctx is None or the debug or verbose flags are
set in context ctx and key is empty or not in outputted_warnings. If key is
not empty and not in outputted_warnings, it is added to it. If msg is empty
'No support for type "' + key + '", defaulting to string.' is printed.
"""
if ((not key or key not in outputted_warnings) and
(not ctx or ctx.opts.debug or ctx.opts.verbose)):
if msg:
sys.stderr.write('WARNING: ' + msg)
if key:
outputted_warnings.append(key)
else:
print_warning(('No support for type "' + key + '", defaulting ' +
'to string.'), key, ctx)
def write_file(d, file_name, file_content, ctx):
"""Creates the directory d if it does not yet exist and writes a file to it
named file_name with file_content in it.
"""
d = d.replace('.', os.sep)
wd = os.getcwd()
try:
os.makedirs(d, 0o777)
except OSError as exc:
if exc.errno == errno.EEXIST:
pass # The directory already exists
else:
raise
try:
os.chdir(d)
except OSError as exc:
if exc.errno == errno.ENOTDIR:
print_warning(msg=('Unable to change directory to ' + d +
'. Probably a non-directory file with same name as one of ' +
'the subdirectories already exists.'), key=d, ctx=ctx)
else:
raise
finally:
if ctx.opts.verbose:
print('Writing file to: ' + os.getcwd() + os.sep + file_name)
os.chdir(wd)
with open(d + os.sep + file_name, 'w+') as f:
if isinstance(file_content, str):
f.write(file_content)
else:
for line in file_content:
f.write(line)
f.write('\n')
def get_module(stmt):
"""Returns the module to which stmt belongs to"""
if stmt.top is not None:
return get_module(stmt.top)
elif stmt.keyword == 'module':
return stmt
else: # stmt.keyword == 'submodule':
belongs_to = search_one(stmt, 'belongs-to')
for (module_name, revision) in stmt.i_ctx.modules:
if module_name == belongs_to.arg:
return stmt.i_ctx.modules[(module_name, revision)]
def get_parent(stmt):
"""Returns closest parent which is not a choice, case or submodule
statement. If the parent is a submodule statement, the corresponding main
module is returned instead.
"""
if stmt.parent is None:
return None
elif stmt.parent.keyword == 'submodule':
return get_module(stmt)
elif stmt.parent.parent is None:
return stmt.parent
elif stmt.parent.keyword in ('choice', 'case'):
return get_parent(stmt.parent)
else:
return stmt.parent
def get_package(stmt, ctx):
"""Returns a string representing the package name of a java class generated
from stmt, assuming that it has been or will be generated by JNC.
"""
sub_packages = collections.deque()
parent = get_parent(stmt)
while parent is not None:
stmt = parent
parent = get_parent(stmt)
sub_packages.appendleft(camelize(stmt.arg))
full_package = ctx.rootpkg.split(os.sep)
full_package.extend(sub_packages)
return '.'.join(full_package)
def pairwise(iterable):
"""Returns an iterator that includes the next item also"""
iterator = iter(iterable)
item = next(iterator) # throws StopIteration if empty.
for next_item in iterator:
yield (item, next_item)
item = next_item
yield (item, None)
def capitalize_first(string):
"""Returns string with its first character capitalized (if any)"""
return string[:1].capitalize() + string[1:]
def decapitalize_first(string):
"""Returns string with its first character decapitalized (if any)"""
return string[:1].lower() + string[1:]
def camelize(string):
"""Converts string to lower camel case
Removes hyphens and dots and replaces following character (if any) with
its upper-case counterpart. Does not remove consecutive or trailing hyphens
or dots.
If the resulting string is reserved in Java, an underline is appended
Returns an empty string if string argument is None. Otherwise, returns
string decapitalized and with no consecutive upper case letters.
"""
try: # Fetch from cache
return camelized_stmt_args[string]
except KeyError:
pass
camelized_str = collections.deque()
if string is not None:
iterator = pairwise(decapitalize_first(string))
for character, next_character in iterator:
if next_character is None:
camelized_str.append(character.lower())
elif character in '-.':
camelized_str.append(capitalize_first(next_character))
next(iterator)
elif (character.isupper()
and (next_character.isupper()
or not next_character.isalpha())):
camelized_str.append(character.lower())
else:
camelized_str.append(character)
res = ''.join(camelized_str)
if res in java_reserved_words | java_literals:
camelized_str.append('_')
if re.match(r'\d', res):
camelized_str.appendleft('_')
res = ''.join(camelized_str)
camelized_stmt_args[string] = res # Add to cache
return res
def normalize(string):
"""returns capitalize_first(camelize(string)), except if camelize(string)
begins with and/or ends with a single underline: then they are/it is
removed and a 'J' is prepended. Mimics normalize in YangElement of JNC.
"""
try: # Fetch from cache
return normalized_stmt_args[string]
except KeyError:
pass
res = camelize(string)
start = 1 if res.startswith('_') else 0
end = -1 if res.endswith('_') else 0
if start or end:
res = 'J' + capitalize_first(res[start:end])
else:
res = capitalize_first(res)
normalized_stmt_args[string] = res # Add to cache
return res
def flatten(l):
"""Returns a flattened version of iterable l
l must not have an attribute named values unless the return value values()
is a valid substitution of l. Same applies to all items in l.
Example: flatten([['12', '34'], ['56', ['7']]]) = ['12', '34', '56', '7']
"""
res = []
while hasattr(l, 'values'):
l = list(l.values())
for item in l:
try:
assert not isinstance(item, str)
iter(item)
except (AssertionError, TypeError):
res.append(item)
else:
res.extend(flatten(item))
return res
def get_types(yang_type, ctx):
"""Returns jnc and primitive counterparts of yang_type, which is a type,
typedef, leaf or leaf-list statement.
"""
if yang_type.keyword in leaf_stmts:
yang_type = search_one(yang_type, 'type')
assert yang_type.keyword in ('type', 'typedef'), 'argument is type, typedef or leaf'
if yang_type.arg == 'leafref':
return get_types(yang_type.parent.i_leafref.i_target_node, ctx)
primitive = normalize(yang_type.arg)
if yang_type.keyword == 'typedef':
primitive = normalize(get_base_type(yang_type).arg)
if primitive == 'JBoolean':
primitive = 'Boolean'
jnc = 'com.tailf.jnc.Yang' + primitive
if yang_type.arg in ('string', 'boolean'):
pass
elif yang_type.arg in ('enumeration', 'binary', 'union', 'empty',
'instance-identifier', 'identityref'):
primitive = 'String'
elif yang_type.arg in ('bits',): # uint64 handled below
primitive = 'BigInteger'
elif yang_type.arg == 'decimal64':
primitive = 'BigDecimal'
elif yang_type.arg in ('int8', 'int16', 'int32', 'int64', 'uint8',
'uint16', 'uint32', 'uint64'):
integer_type = ['long', 'int', 'short', 'byte']
if yang_type.arg[:1] == 'u': # Unsigned
integer_type.pop()
integer_type.insert(0, 'BigInteger')
jnc = 'com.tailf.jnc.YangUI' + yang_type.arg[2:]
if yang_type.arg[-2:] == '64':
primitive = integer_type[0]
elif yang_type.arg[-2:] == '32':
primitive = integer_type[1]
elif yang_type.arg[-2:] == '16':
primitive = integer_type[2]
else: # 8 bits
primitive = integer_type[3]
else:
try:
typedef = yang_type.i_typedef
except AttributeError:
if yang_type.keyword == 'typedef':
primitive = normalize(yang_type.arg)
else:
pkg = get_package(yang_type, ctx)
name = normalize(yang_type.arg)
print_warning(key=pkg + '.' + name, ctx=ctx)
else:
basetype = get_base_type(typedef)
jnc, primitive = get_types(basetype, ctx)
if get_parent(typedef).keyword in ('module', 'submodule'):
package = get_package(typedef, ctx)
typedef_arg = normalize(typedef.arg)
jnc = package + '.' + typedef_arg
return jnc, primitive
def get_base_type(stmt):
"""Returns the built in type that stmt is derived from"""
if stmt.keyword == 'type' and stmt.arg == 'union':
return stmt
type_stmt = search_one(stmt, 'type')
if type_stmt is None:
return stmt
try:
typedef = type_stmt.i_typedef
except AttributeError:
return type_stmt
else:
if typedef is not None:
return get_base_type(typedef)
else:
return type_stmt
def get_import(string):
"""Returns a string representing a class that can be imported in Java.
Does not handle Generics or Array types and is data model agnostic.
"""
if string.startswith(('java.math', 'java.util', 'com.tailf.jnc')):
return string
elif string in ('BigInteger', 'BigDecimal'):
return '.'.join(['java.math', string])
elif string in java_util:
return '.'.join(['java.util', string])
else:
return '.'.join(['com.tailf.jnc', string])
def search(stmt, keywords):
"""Utility for calling Statement.search. If stmt has an i_children
attribute, they are searched for keywords as well.
stmt -- Statement to search for children in
keywords -- A string, or a tuple, list or set of strings, to search for
Returns a set (without duplicates) of children with matching keywords.
If choice or case is not in keywords, substatements of choice and case
are searched as well.
"""
if isinstance(keywords, str):
keywords = keywords.split()
bypassed = ('choice', 'case')
bypass = all(x not in keywords for x in bypassed)
dict_ = collections.OrderedDict()
def iterate(children, acc):
for ch in children:
if bypass and ch.keyword in bypassed:
_search(ch, keywords, acc)
continue
try:
key = ' '.join([ch.keyword, camelize(ch.arg)])
except TypeError:
if ch.arg is None: # Extension
key = ' '.join(ch.keyword)
else:
key = ' '.join([':'.join(ch.keyword), camelize(ch.arg)])
if key in acc:
continue
for keyword in keywords:
if ch.keyword == keyword:
acc[key] = ch
break
def _search(stmt, keywords, acc):
if any(x in keywords for x in ('typedef', 'import',
'augment', 'include')):
old_keywords = keywords[:]
keywords = ['typedef', 'import', 'augment', 'include']
iterate(stmt.substmts, acc)
keywords = old_keywords
try:
iterate(stmt.i_children, acc)
except AttributeError:
iterate(stmt.substmts, acc)
_search(stmt, keywords, dict_)
return list(dict_.values())
def search_one(stmt, keyword, arg=None):
"""Utility for calling Statement.search_one, including i_children."""
res = stmt.search_one(keyword, arg=arg)
if res is None:
try:
res = stmt.search_one(keyword, arg=arg, children=stmt.i_children)
except AttributeError:
pass
if res is None:
try:
return search(stmt, keyword)[0]
except IndexError:
return None
return res
def is_config(stmt):
"""Returns True if stmt is a configuration data statement"""
config = None
while config is None and stmt is not None:
if stmt.keyword == 'notification':
return False # stmt is not config if part of a notification tree
config = search_one(stmt, 'config')
stmt = get_parent(stmt)
return config is None or config.arg == 'true'
class SchemaNode(object):
def __init__(self, stmt, tagpath):
self.stmt = stmt
self.tagpath = tagpath
def as_list(self):
"""Returns a string list repr "node" element content for an XML schema"""
res = ['<node>']
stmt = self.stmt
res.append('<tagpath>' + self.tagpath + '</tagpath>')
top_stmt = get_module(stmt)
if top_stmt.keyword == 'module':
module = top_stmt
else: #submodule
modulename = search_one(top_stmt, 'belongs-to').arg
for (name, rev) in top_stmt.i_ctx.modules:
if name == modulename:
module = top_stmt.i_ctx.modules[(name, rev)]
break
ns = search_one(module, 'namespace').arg
res.append('<namespace>' + ns + '</namespace>')
res.append('<primitive_type>0</primitive_type>')
min_occurs = '0'
max_occurs = '-1'
mandatory = search_one(stmt, 'mandatory')
isMandatory = mandatory is not None and mandatory.arg == 'true'
unique = search_one(stmt, 'unique')
isUnique = unique is not None and unique.arg == 'true'
key = None
parent = get_parent(stmt)
if parent:
key = search_one(parent, 'key')
isKey = key is not None and key.arg == stmt.arg
childOfContainerOrList = (parent
and parent.keyword in yangelement_stmts)
if (isKey or stmt.keyword in ('module', 'submodule')
or (childOfContainerOrList
and stmt.keyword in ('container', 'notification'))):
min_occurs = '1'
max_occurs = '1'
if isMandatory:
min_occurs = '1'
if (isUnique or childOfContainerOrList
or stmt.keyword in ('container', 'notification')):
max_occurs = '1'
res.append('<min_occurs>' + min_occurs + '</min_occurs>')
res.append('<max_occurs>' + max_occurs + '</max_occurs>')
children = ''
for ch in search(stmt, yangelement_stmts | leaf_stmts):
children += camelize(ch.arg) + ' '
res.append('<children>' + children[:-1] + '</children>')
res.append('<flags>0</flags>')
res.append('<desc></desc>')
res.append('</node>')
return res
class SchemaGenerator(object):
"""Used to generate an external XML schema from a yang module"""
def __init__(self, stmts, tagpath, ctx):
self.stmts = stmts
self.tagpath = tagpath
self.ctx = ctx
def schema_nodes(self):
"""Generate XML schema as a list of "node" elements"""
res = []
for stmt in self.stmts:
subpath = self.tagpath + stmt.arg + '/'
if self.ctx.opts.verbose:
print('Generating schema node "' + subpath + '"...')
node = SchemaNode(stmt, subpath)
res.extend(node.as_list())
substmt_generator = SchemaGenerator(search(stmt, node_stmts),
subpath, self.ctx)
res.extend(substmt_generator.schema_nodes())
return res
class YangType(object):
"""Provides an interface to maintain a list of defined yang types"""
def __init__(self):
self.defined_types = ['empty', 'int8', 'int16', 'int32', 'int64',
'uint8', 'uint16', 'uint32', 'uint64', 'binary', 'bits', 'boolean',
'decimal64', 'enumeration', 'identityref', 'instance-identifier',
'leafref', 'string', 'union'] # Use set instead!
"""List of types represented by a jnc or generated class"""
def defined(self, yang_type):
"""Returns true if yang_type is defined, else false"""
return (yang_type in self.defined_types)
def add(self, yang_type):
"""Gives yang_type "defined" status in this instance of YangType"""
self.defined_types.append(yang_type)
class ClassGenerator(object):
"""Used to generate java classes from a yang module"""
def __init__(self, stmt, path=None, package=None, src=None, ctx=None,
ns='', prefix_name='', yang_types=None, parent=None):
"""Constructor.
stmt -- A statement (sub)tree, parsed from a YANG model
path -- Full path to where the class should be written
package -- Name of Java package
src -- Filename of parsed yang module, or the module name and
revision if filename is unknown
ctx -- Context used to fetch option parameters
ns -- The XML namespace of the module
prefix_name -- The module prefix
yang_types -- An instance of the YangType class
parent -- ClassGenerator to copy arguments that were not supplied
from (if applicable)
"""
self.stmt = stmt
self.path = path
self.package = None if package is None else package.replace(os.sep, '.')
self.src = src
self.ctx = ctx
self.ns = ns
self.prefix_name = prefix_name
self.yang_types = yang_types
self.n = normalize(stmt.arg)
self.n2 = camelize(stmt.arg)
if stmt.keyword in module_stmts:
self.filename = normalize(search_one(stmt, 'prefix').arg) + '.java'
else:
self.filename = self.n + '.java'
if yang_types is None:
self.yang_types = YangType()
if parent is not None:
for attr in ('package', 'src', 'ctx', 'path', 'ns',
'prefix_name', 'yang_types'):
if getattr(self, attr) is None:
setattr(self, attr, getattr(parent, attr))
module = get_module(stmt)
if self.ctx.rootpkg:
self.rootpkg = '.'.join([self.ctx.rootpkg.replace(os.sep, '.'),
camelize(module.arg)])
else:
self.rootpkg = camelize(module.arg)
else:
self.rootpkg = package
def generate(self):
"""Generates class(es) for self.stmt"""
if self.stmt.keyword in ('module', 'submodule'):
self.generate_classes()
else:
self.generate_class()
def generate_classes(self):
"""Generates a Java class hierarchy from a module statement, allowing
for netconf communication using the jnc library.
"""
assert(self.stmt.keyword == 'module')
# Namespace and prefix
ns_arg = search_one(self.stmt, 'namespace').arg