+#
+# Copyright 2012-2015 Bronto Software, Inc. and contributors
+#
+# 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.
+#
+
+import re
+import string
+
+from docutils import nodes
+from docutils.parsers.rst import Directive, directives
+
+from sphinx import addnodes, version_info
+from sphinx.roles import XRefRole
+from sphinx.locale import l_, _
+from sphinx.domains import Domain, ObjType
+from sphinx.directives import ObjectDescription
+from sphinx.util.nodes import make_refnode
+from sphinx.util.docfields import Field, TypedField, GroupedField
+
+import javalang
+
+import javasphinx.extdoc as extdoc
+import javasphinx.formatter as formatter
+import javasphinx.util as util
+
+# Classes in java.lang. These are available without an import.
+java_dot_lang = set([
+ 'AbstractMethodError', 'Appendable', 'ArithmeticException',
+ 'ArrayIndexOutOfBoundsException', 'ArrayStoreException', 'AssertionError',
+ 'AutoCloseable', 'Boolean', 'BootstrapMethodError', 'Byte', 'Character',
+ 'CharSequence', 'Class', 'ClassCastException', 'ClassCircularityError',
+ 'ClassFormatError', 'ClassLoader', 'ClassNotFoundException', 'ClassValue',
+ 'Cloneable', 'CloneNotSupportedException', 'Comparable', 'Compiler',
+ 'Deprecated', 'Double', 'Enum', 'EnumConstantNotPresentException', 'Error',
+ 'Exception', 'ExceptionInInitializerError', 'Float', 'IllegalAccessError',
+ 'IllegalAccessException', 'IllegalArgumentException',
+ 'IllegalMonitorStateException', 'IllegalStateException',
+ 'IllegalThreadStateException', 'IncompatibleClassChangeError',
+ 'IndexOutOfBoundsException', 'InheritableThreadLocal', 'InstantiationError',
+ 'InstantiationException', 'Integer', 'InternalError', 'InterruptedException',
+ 'Iterable', 'LinkageError', 'Long', 'Math', 'NegativeArraySizeException',
+ 'NoClassDefFoundError', 'NoSuchFieldError', 'NoSuchFieldException',
+ 'NoSuchMethodError', 'NoSuchMethodException', 'NullPointerException', 'Number',
+ 'NumberFormatException', 'Object', 'OutOfMemoryError', 'Override', 'Package',
+ 'Process', 'ProcessBuilder', 'Readable', 'ReflectiveOperationException',
+ 'Runnable', 'Runtime', 'RuntimeException', 'RuntimePermission', 'SafeVarargs',
+ 'SecurityException', 'SecurityManager', 'Short', 'StackOverflowError',
+ 'StackTraceElement', 'StrictMath', 'String', 'StringBuffer', 'StringBuilder',
+ 'StringIndexOutOfBoundsException', 'SuppressWarnings', 'System', 'Thread',
+ 'ThreadDeath', 'ThreadGroup', 'ThreadLocal', 'Throwable',
+ 'TypeNotPresentException', 'UnknownError', 'UnsatisfiedLinkError',
+ 'UnsupportedClassVersionError', 'UnsupportedOperationException', 'VerifyError',
+ 'VirtualMachineError', 'Void'])
+
+class JavaObject(ObjectDescription):
+ option_spec = {
+ 'noindex': directives.flag,
+ 'package': directives.unchanged,
+ 'outertype': directives.unchanged
+ }
+
+ def _build_ref_node(self, target):
+ ref = addnodes.pending_xref('', refdomain='java', reftype='type', reftarget=target, modname=None, classname=None)
+ ref['java:outertype'] = self.get_type()
+
+ package = self.env.temp_data.get('java:imports', dict()).get(target, None)
+
+ if not package and target in java_dot_lang:
+ package = 'java.lang'
+
+ if package:
+ ref['java:imported'] = True
+ ref['java:package'] = package
+ else:
+ ref['java:imported'] = False
+ ref['java:package'] = self.get_package()
+
+ return ref
+
+ def _build_type_node(self, typ):
+ if isinstance(typ, javalang.tree.ReferenceType):
+ if typ.dimensions:
+ dim = '[]' * len(typ.dimensions)
+ else:
+ dim = ''
+
+ target = typ.name
+ parts = []
+
+ while typ:
+ ref_node = self._build_ref_node(target)
+ ref_node += nodes.Text(typ.name, typ.name)
+ parts.append(ref_node)
+
+ if typ.arguments:
+ parts.append(nodes.Text('<', '<'))
+
+ first = True
+ for type_arg in typ.arguments:
+ if first:
+ first = False
+ else:
+ parts.append(nodes.Text(', ', ', '))
+
+ if type_arg.pattern_type == '?':
+ parts.append(nodes.Text('?', '?'))
+ else:
+ if type_arg.pattern_type:
+ s = '? %s ' % (type_arg.pattern_type,)
+ parts.append(nodes.Text(s, s))
+ parts.extend(self._build_type_node(type_arg.type))
+
+ parts.append(nodes.Text('>', '>'))
+
+ typ = typ.sub_type
+
+ if typ:
+ target = target + '.' + typ.name
+ parts.append(nodes.Text('.', '.'))
+ elif dim:
+ parts.append(nodes.Text(dim, dim))
+
+ return parts
+ else:
+ type_repr = formatter.output_type(typ).build()
+ return [nodes.Text(type_repr, type_repr)]
+
+ def _build_type_node_list(self, types):
+ parts = self._build_type_node(types[0])
+ for typ in types[1:]:
+ parts.append(nodes.Text(', ', ', '))
+ parts.extend(self._build_type_node(typ))
+ return parts
+
+ def handle_signature(self, sig, signode):
+ handle_name = 'handle_%s_signature' % (self.objtype,)
+ handle = getattr(self, handle_name, None)
+
+ if handle:
+ return handle(sig, signode)
+ else:
+ raise NotImplementedError
+
+ def get_index_text(self, package, type, name):
+ raise NotImplementedError
+
+ def get_package(self):
+ return self.options.get('package', self.env.temp_data.get('java:package'))
+
+ def get_type(self):
+ return self.options.get('outertype', '.'.join(self.env.temp_data.get('java:outertype', [])))
+
+ def add_target_and_index(self, name, sig, signode):
+ package = self.get_package()
+ type = self.get_type();
+
+ fullname = '.'.join(filter(None, (package, type, name)))
+ basename = fullname.partition('(')[0]
+
+ # note target
+ if fullname not in self.state.document.ids:
+ signode['names'].append(fullname)
+ signode['ids'].append(fullname)
+ signode['first'] = (not self.names)
+ self.state.document.note_explicit_target(signode)
+
+ objects = self.env.domaindata['java']['objects']
+ if fullname in objects:
+ self.state_machine.reporter.warning(
+ 'duplicate object description of %s, ' % fullname +
+ 'other instance in ' + self.env.doc2path(objects[fullname][0]) +
+ ', use :noindex: for one of them',
+ line=self.lineno)
+
+ objects[fullname] = (self.env.docname, self.objtype, basename)
+
+ indextext = self.get_index_text(package, type, name)
+ if indextext:
+ self.indexnode['entries'].append(_create_indexnode(indextext, fullname))
+
+ def before_content(self):
+ self.set_type = False
+
+ if self.objtype == 'type' and self.names:
+ self.set_type = True
+ self.env.temp_data.setdefault('java:outertype', list()).append(self.names[0])
+
+ def after_content(self):
+ if self.set_type:
+ self.env.temp_data['java:outertype'].pop()
+
+class JavaMethod(JavaObject):
+ doc_field_types = [
+ TypedField('parameter', label=l_('Parameters'),
+ names=('param', 'parameter', 'arg', 'argument'),
+ typerolename='type', typenames=('type',)),
+ Field('returnvalue', label=l_('Returns'), has_arg=False,
+ names=('returns', 'return')),
+ GroupedField('throws', names=('throws',), label=l_('Throws'), rolename='type')
+ ]
+
+ def handle_method_signature(self, sig, signode):
+ try:
+ member = javalang.parse.parse_member_signature(sig)
+ except javalang.parser.JavaSyntaxError:
+ raise self.error("syntax error in method signature")
+
+ if not isinstance(member, javalang.tree.MethodDeclaration):
+ raise self.error("expected method declaration")
+
+ mods = formatter.output_modifiers(member.modifiers).build()
+ signode += nodes.Text(mods + ' ', mods + ' ')
+
+ if member.type_parameters:
+ type_params = formatter.output_type_params(member.type_parameters).build()
+ signode += nodes.Text(type_params, type_params)
+ signode += nodes.Text(' ', ' ')
+
+ rnode = addnodes.desc_type('', '')
+ rnode += self._build_type_node(member.return_type)
+
+ signode += rnode
+ signode += nodes.Text(' ', ' ')
+ signode += addnodes.desc_name(member.name, member.name)
+
+ paramlist = addnodes.desc_parameterlist()
+ for parameter in member.parameters:
+ param = addnodes.desc_parameter('', '', noemph=True)
+ param += self._build_type_node(parameter.type)
+
+ if parameter.varargs:
+ param += nodes.Text('...', '')
+
+ param += nodes.emphasis(' ' + parameter.name, ' ' + parameter.name)
+ paramlist += param
+ signode += paramlist
+
+ param_reprs = [formatter.output_type(param.type, with_generics=False).build() for param in member.parameters]
+ return member.name + '(' + ', '.join(param_reprs) + ')'
+
+ def get_index_text(self, package, type, name):
+ return _('%s (Java method)' % (name,))
+
+class JavaConstructor(JavaObject):
+ doc_field_types = [
+ TypedField('parameter', label=l_('Parameters'),
+ names=('param', 'parameter', 'arg', 'argument'),
+ typerolename='type', typenames=('type',)),
+ GroupedField('throws', names=('throws',), label=l_('Throws'))
+ ]
+
+ def handle_constructor_signature(self, sig, signode):
+ try:
+ member = javalang.parse.parse_constructor_signature(sig)
+ except javalang.parser.JavaSyntaxError:
+ raise self.error("syntax error in constructor signature")
+
+ if not isinstance(member, javalang.tree.ConstructorDeclaration):
+ raise self.error("expected constructor declaration")
+
+ mods = formatter.output_modifiers(member.modifiers).build()
+ signode += nodes.Text(mods + ' ', mods + ' ')
+
+ signode += addnodes.desc_name(member.name, member.name)
+
+ paramlist = addnodes.desc_parameterlist()
+ for parameter in member.parameters:
+ param = addnodes.desc_parameter('', '', noemph=True)
+ param += self._build_type_node(parameter.type)
+
+ if parameter.varargs:
+ param += nodes.Text('...', '')
+
+ param += nodes.emphasis(' ' + parameter.name, ' ' + parameter.name)
+ paramlist += param
+ signode += paramlist
+
+ param_reprs = [formatter.output_type(param.type, with_generics=False).build() for param in member.parameters]
+ return '%s(%s)' % (member.name, ', '.join(param_reprs))
+
+ def get_index_text(self, package, type, name):
+ return _('%s (Java constructor)' % (name,))
+
+class JavaType(JavaObject):
+ doc_field_types = [
+ GroupedField('parameter', names=('param',), label=l_('Parameters'))
+ ]
+
+ declaration_type = None
+
+ def handle_type_signature(self, sig, signode):
+ try:
+ member = javalang.parse.parse_type_signature(sig)
+ except javalang.parser.JavaSyntaxError:
+ raise self.error("syntax error in field signature")
+
+ if isinstance(member, javalang.tree.ClassDeclaration):
+ self.declaration_type = 'class'
+ elif isinstance(member, javalang.tree.InterfaceDeclaration):
+ self.declaration_type = 'interface'
+ elif isinstance(member, javalang.tree.EnumDeclaration):
+ self.declaration_type = 'enum'
+ elif isinstance(member, javalang.tree.AnnotationDeclaration):
+ self.declaration_type = 'annotation'
+ else:
+ raise self.error("expected type declaration")
+
+ mods = formatter.output_modifiers(member.modifiers).build()
+ signode += nodes.Text(mods + ' ', mods + ' ')
+
+ if self.declaration_type == 'class':
+ signode += nodes.Text('class ', 'class ')
+ elif self.declaration_type == 'interface':
+ signode += nodes.Text('interface ', 'interface ')
+ elif self.declaration_type == 'enum':
+ signode += nodes.Text('enum ', 'enum ')
+ elif self.declaration_type == 'annotation':
+ signode += nodes.Text('@interface ', '@interface ')
+
+ signode += addnodes.desc_name(member.name, member.name)
+
+ if self.declaration_type in ('class', 'interface') and member.type_parameters:
+ type_params = formatter.output_type_params(member.type_parameters).build()
+ signode += nodes.Text(type_params, type_params)
+
+ if self.declaration_type == 'class':
+ if member.extends:
+ extends = ' extends '
+ signode += nodes.Text(extends, extends)
+ signode += self._build_type_node(member.extends)
+ if member.implements:
+ implements = ' implements '
+ signode += nodes.Text(implements, implements)
+ signode += self._build_type_node_list(member.implements)
+ elif self.declaration_type == 'interface':
+ if member.extends:
+ extends = ' extends '
+ signode += nodes.Text(extends, extends)
+ signode += self._build_type_node_list(member.extends)
+ elif self.declaration_type == 'enum':
+ if member.implements:
+ implements = ' implements '
+ signode += nodes.Text(implements, implements)
+ signode += self._build_type_node_list(member.implements)
+
+ return member.name
+
+ def get_index_text(self, package, type, name):
+ return _('%s (Java %s)' % (name, self.declaration_type))
+
+class JavaField(JavaObject):
+ def handle_field_signature(self, sig, signode):
+ try:
+ member = javalang.parse.parse_member_signature(sig)
+ except javalang.parser.JavaSyntaxError:
+ raise self.error("syntax error in field signature")
+
+ if not isinstance(member, javalang.tree.FieldDeclaration):
+ raise self.error("expected field declaration")
+
+ mods = formatter.output_modifiers(member.modifiers).build()
+ signode += nodes.Text(mods + ' ', mods + ' ')
+
+ tnode = addnodes.desc_type('', '')
+ tnode += self._build_type_node(member.type)
+
+ signode += tnode
+ signode += nodes.Text(' ', ' ')
+
+ if len(member.declarators) > 1:
+ self.error('only one field may be documented at a time')
+
+ declarator = member.declarators[0]
+ signode += addnodes.desc_name(declarator.name, declarator.name)
+
+ dim = '[]' * len(declarator.dimensions)
+ signode += nodes.Text(dim)
+
+ if declarator.initializer and isinstance(declarator.initializer, javalang.tree.Literal):
+ signode += nodes.Text(' = ' + declarator.initializer.value)
+
+ return declarator.name
+
+ def get_index_text(self, package, type, name):
+ return _('%s (Java field)' % (name,))
+
+class JavaPackage(Directive):
+ """
+ Directive to mark description of a new package.
+ """
+
+ has_content = False
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = False
+ option_spec = {
+ 'noindex': directives.flag,
+ }
+
+ def run(self):
+ env = self.state.document.settings.env
+ package = self.arguments[0].strip()
+ noindex = 'noindex' in self.options
+ env.temp_data['java:package'] = package
+ env.domaindata['java']['objects'][package] = (env.docname, 'package', package)
+ ret = []
+
+ if not noindex:
+ targetnode = nodes.target('', '', ids=['package-' + package], ismod=True)
+ self.state.document.note_explicit_target(targetnode)
+
+ # the platform and synopsis aren't printed; in fact, they are only
+ # used in the modindex currently
+ ret.append(targetnode)
+
+ indextext = _('%s (package)') % (package,)
+ inode = addnodes.index(entries=[_create_indexnode(indextext, 'package-' + package)])
+ ret.append(inode)
+
+ return ret
+
+class JavaImport(Directive):
+ """
+ This directive is just to tell Sphinx the source of a referenced type.
+ """
+
+ has_content = False
+ required_arguments = 2
+ optional_arguments = 0
+ final_argument_whitespace = False
+ option_spec = {}
+
+ def run(self):
+ env = self.state.document.settings.env
+ package, typename = self.arguments
+
+ env.temp_data.setdefault('java:imports', dict())[typename] = package
+ return []
+
+class JavaXRefRole(XRefRole):
+ def process_link(self, env, refnode, has_explicit_title, title, target):
+ refnode['java:outertype'] = '.'.join(env.temp_data.get('java:outertype', list()))
+
+ target = target.lstrip('~')
+
+ # Strip a method component from the target
+ basetype = target
+ if '(' in basetype:
+ basetype = basetype.partition('(')[0]
+ if '.' in basetype:
+ basetype = basetype.rpartition('.')[0]
+
+ package = env.temp_data.get('java:imports', dict()).get(basetype, None)
+
+ if package:
+ refnode['java:imported'] = True
+ refnode['java:package'] = package
+ else:
+ refnode['java:imported'] = False
+ refnode['java:package'] = env.temp_data.get('java:package')
+
+ if not has_explicit_title:
+ # if the first character is a tilde, don't display the module/class
+ # parts of the contents
+ if title[0:1] == '~':
+ title = title.partition('(')[0]
+ title = title[1:]
+ dot = title.rfind('.')
+ if dot != -1:
+ title = title[dot+1:]
+
+ return title, target
+
+class JavaDomain(Domain):
+ """Java language domain."""
+ name = 'java'
+ label = 'Java'
+
+ object_types = {
+ 'package': ObjType(l_('package'), 'package', 'ref'),
+ 'type': ObjType(l_('type'), 'type', 'ref'),
+ 'field': ObjType(l_('field'), 'field', 'ref'),
+ 'constructor': ObjType(l_('constructor'), 'construct', 'ref'),
+ 'method': ObjType(l_('method'), 'meth', 'ref')
+ }
+
+ directives = {
+ 'package': JavaPackage,
+ 'type': JavaType,
+ 'field': JavaField,
+ 'constructor': JavaConstructor,
+ 'method': JavaMethod,
+ 'import': JavaImport
+ }
+
+ roles = {
+ 'package': JavaXRefRole(),
+ 'type': JavaXRefRole(),
+ 'field': JavaXRefRole(),
+ 'construct': JavaXRefRole(),
+ 'meth': JavaXRefRole(),
+ 'ref': JavaXRefRole(),
+ }
+
+ initial_data = {
+ 'objects': {}, # fullname -> docname, objtype, basename
+ }
+
+ def clear_doc(self, docname):
+ objects = dict(self.data['objects'])
+
+ for fullname, (fn, _, _) in objects.items():
+ if fn == docname:
+ del self.data['objects'][fullname]
+
+ def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode):
+ objects = self.data['objects']
+ package = node.get('java:package')
+ imported = node.get('java:imported')
+ type_context = node.get('java:outertype')
+
+ # Partial function to make building the response easier
+ make_ref = lambda fullname: make_refnode(builder, fromdocname, objects[fullname][0], fullname, contnode, fullname)
+
+ # Check for fully qualified references
+ if target in objects:
+ return make_ref(target)
+
+ # Try with package name prefixed
+ if package:
+ fullname = package + '.' + target
+ if fullname in objects:
+ return make_ref(fullname)
+
+ # Try with package and type prefixed
+ if package and type_context:
+ fullname = package + '.' + type_context + '.' + target
+ if fullname in objects:
+ return make_ref(fullname)
+
+ # Try to find a matching suffix
+ suffix = '.' + target
+ basename_match = None
+ basename_suffix = suffix.partition('(')[0]
+
+ for fullname, (_, _, basename) in objects.items():
+ if fullname.endswith(suffix):
+ return make_ref(fullname)
+ elif basename.endswith(basename_suffix):
+ basename_match = fullname
+
+ if basename_match:
+ return make_ref(basename_match)
+
+ # Try creating an external documentation reference
+ ref = extdoc.get_javadoc_ref(self.env, target, target)
+
+ if not ref and target in java_dot_lang:
+ fulltarget = 'java.lang.' + target
+ ref = extdoc.get_javadoc_ref(self.env, fulltarget, fulltarget)
+
+ # If the target was imported try with the package prefixed
+ if not ref and imported:
+ fulltarget = package + '.' + target
+ ref = extdoc.get_javadoc_ref(self.env, fulltarget, fulltarget)
+
+ if ref:
+ ref.append(contnode)
+ return ref
+ else:
+ return None
+
+ def get_objects(self):
+ for refname, (docname, type, _) in self.data['objects'].items():
+ yield (refname, refname, type, docname, refname, 1)
+
+
+def _create_indexnode(indextext, fullname):
+ # See https://github.com/sphinx-doc/sphinx/issues/2673
+ if version_info < (1, 4):
+ return ('single', indextext, fullname, '')
+ else:
+ return ('single', indextext, fullname, '', None)