"""
Services, i.e. combinations of a core and at least one renderer.

See the __init__.py docstring for a bit more on the general
architecture.
"""

#c Copyright 2008-2025, the GAVO project <gavo@ari.uni-heidelberg.de>
#c
#c This program is free software, covered by the GNU GPL.  See the
#c COPYING file in the source distribution.


import functools
import os
import urllib.parse

from twisted.web.template import tags as T #noflake: namespace registration

from gavo import base
from gavo import rsc
from gavo import rscdef
from gavo import utils
from gavo.base import meta
from gavo.formal import nevowc
from gavo.rsc import table
from gavo.rscdef import rmkdef
from gavo.svcs import common
from gavo.svcs import core
from gavo.svcs import inputdef
from gavo.svcs import outputdef
from gavo.svcs import renderers


MS = base.makeStruct


class PreparsedInput(dict):
	"""a sentinel class signalling to the service that its input already
	is parsed.

	This is for for stuff coming from nevow formal rather than request.strargs,
	and to be fed into service.run.

	Construct with a dictionary.
	"""


class Publication(base.Structure, base.ComputedMetaMixin):
	"""A specification of how a service should be published.

	This contains most of the metadata for what is an interface in
	registry speak.
	"""
	name_ = "publish"

	_rd = rscdef.RDAttribute()
	_render = base.UnicodeAttribute("render", default=base.Undefined,
		description="The renderer the publication will point at.",
		copyable=True)
	_sets = base.StringSetAttribute("sets",
		description="Comma-separated list of sets this service will be"
			" published in.  Predefined are: local=publish on front page,"
			" ivo_managed=register with the VO registry.  If you leave it"
			" empty, 'local' publication is assumed.",
			copyable="True")
	_service = base.ReferenceAttribute("service", default=base.NotGiven,
		description="Reference for a service actually implementing the"
			" capability corresponding to this publication.  This is"
			" mainly when there is a vs:WebBrowser service accompanying a VO"
			" protocol service, and this other service should be published"
			" in the same resource record; you would say something like"
			' ``<publish render="form" sets="..." service="web"/>``.',
			copyable="True")
	_auxiliary = base.BooleanAttribute("auxiliary", default=False,
		description="Auxiliary publications are for capabilities"
			" not intended to be picked up for all-VO queries, typically"
			" because they are already registered with other services."
			" This is mostly used internally; you probably have no reason"
			" to touch it.")

	def __repr__(self):
		return "<%sPublication of %s in %s>"%(
			"Auxiliary " if self.auxiliary else "",
			self.service.getFullId(),
			self.sets)

	def _completeMetadataFromResRow(self, resRow):
		"""fiddles publication dates from a dc.resources row for the parent
		service.

		(see rscdef.rdjinj for where this comes from).
		"""
		if resRow.get("rectimestamp"):
			self.service.setMeta("_metadataUpdated", resRow["rectimestamp"])
		# we ignore dateupdated here, assuming that the info coming from
		# the RD is more current.

	def completeElement(self, ctx):
		if self.render is base.Undefined:
			self.render = "form"
		if not self.sets:
			self.sets.add("local")
		if self.service is base.NotGiven:
			self.service = self.parent
		self.setMetaParent(self.service)

		if (ctx is not None
				# RD-less services are probably testing-only, and they certainly
				# won't be registered.
				and self.rd is not None):
			self._completeMetadataFromResRow(
				ctx.getInjected("resprop:%s#%s"%(
					self.service.rd.sourceId, self.service.id), {}))
		
		super().completeElement(ctx)

	def validate(self):
		super().validate()
		try:
			renderers.getRenderer(self.render)
		except KeyError:
			raise base.StructureError("Unknown renderer: %s"%self.render)

		unknownSets = self.sets-set(base.getConfig("ivoa", "validoaisets"))
		if unknownSets:
			raise base.StructureError("Invalid OAI set(s) %s"%
					", ".join(unknownSets),
				hint="If you really want to use custom sets, declare add them"
				" using the [ivoa]validOAISets config item.")

	def _meta_accessURL(self):
		return self.service.getURL(self.render, canonical=True)

	def _meta_urlUse(self):
		return renderers.getRenderer(self.render).urlUse

	def _meta_requestMethod(self):
		return renderers.getRenderer(self.render).preferredMethod
	
	def _meta_resultType(self):
		return renderers.getRenderer(self.render).resultType


class _inevowFake(object):
	"""A minimal fake for DaCHS1 imp.nevow.inevow.
	
	DaCHS 1 docs instructed users to get the request object using
	inevow.IRequest(ctx).  This thing lets people run RDs doing that.

	I guess we won't remove it any time soon -- it's not expensive, and
	removing it would probably break quite a few RDs.
	"""
	@classmethod
	def IRequest(cls, ctx):
		return ctx.request


class CustomPageFunction(base.Structure, base.RestrictionMixin):
	"""An abstract base for t.w.resource.Resource-related functions on services.
	"""
	_name = base.UnicodeAttribute("name", default=base.Undefined,
		description="Name of the render or data function (use this in the"
			" n:render or n:data attribute in custom templates).",
			copyable=True, strip=True)
	_code = base.DataContent(description="Function body of the renderer"
		" or data function; the"
		" arguments are named ``request`` and ``tag``, but you can also use"
		" legacy ctx.tag and ctx.request.  If data has been set on a tag,"
		" you will see it as ``data``.", copyable=True)

	def onElementComplete(self):
		super().onElementComplete()
		vars = globals().copy()
		vars["service"] = self.parent
		vars["_FakeContext"] = _FakeContext
		vars["inevow"] = _inevowFake
		exec("\n".join([
			"def %s(self, request, tag):"%(self.name),
			"  ctx = _FakeContext(tag, request)",
			"  data = tag.slotData",
			utils.fixIndentation(self.content_, newIndent="  ",
				governingLine=1).rstrip()]), vars)
		self.func = vars[self.name]


class _FakeContext(object):
	"""A compatibility wrapper for legacy nevow contexts.

	These don't exist any more, but I didn't want to break all custom
	render and data functions in RDs, so I fake enough to give people
	the standard ctx.tag.  There's even a fake invevow above that's just
	enough to pull the request from here.
	"""
	def __init__(self, tag, request):
		self.tag, self.request = tag, request


class CustomRF(CustomPageFunction):
	"""A custom render function for a service.

	Custom render functions can be used to expose certain aspects of a service
	to Nevow templates.  Thus, their definition usually only makes sense with
	custom templates, though you could, in principle, override built-in
	render functions.

	In the render functions, you have the names ctx for a context and
	data for the "current data" (i.e., what's last been set using n:data).
	In ctx, only use ctx.tag (the tag on which the n:render attribute
	sits) and, if necessary ctx.request (the t.w request object).

	Also, the active renderer is visible as self; the one thing you
	might want to see from there is self.queryMeta, which contains,
	for instance, the input parameters (but be careful: the inputTable
	will be None when input errors are rendered, so it is better to write
	something like::

	  if self.queryMeta["inputTable"] and self.queryMeta["inputTable"]...:

	You can return anything that can be in a stan DOM.  Usually, this will be
	a string.  To return HTML, use the stan DOM available under the T namespace.

	As an example, the following code returns the current data as a link::

	  return ctx.tag[T.a(href=data)[data]]
	
	You can access the embedding service as service, the embedding
	RD as service.rd.
	"""
	name_ = "customRF"


class CustomDF(CustomPageFunction):
	"""A custom data function for a service.

	Custom data functions can be used to expose certain aspects of a service
	to Nevow templates.  Thus, their definition usually only makes sense with
	custom templates, though you could, in principle, override built-in
	render functions.

	In the data functions, you have the names ctx for a context and
	data for the "current data" (i.e., what's last been set using n:data).
	In ctx, only use ctx.tag (the tag on which the n:render attribute
	sits) and, if necessary ctx.request (the t.w request object).

	Also, the active renderer is visible as self; the one thing you
	might want to see from there is self.queryMeta, which contains,
	for instance, the input parameters.


	You can access the embedding service as service, the embedding
	RD as service.rd.

	You can return arbitrary python objects -- whatever the render functions
	can deal with.  You could, e.g., write::

	  <customDF name="now">
	    return datetime.datetime.utcnow()
	  </customDF>
	
	You can use the request to fetch request parameters.  Within DaCHS,
	in addition to the clumsy request.args (mapping bytes to bytes), there
	is also request.strargs (mapping strings to strings).  So, access a
	query parameter ``order`` like this::

	  sortOrder = ctx.request.strargs.get("order", ["authors"])
	"""
	name_ = "customDF"


class CoreAttribute(base.ReferenceAttribute):
	def __init__(self):
		base.ReferenceAttribute.__init__(self, "core",
			description="The core that does the computations for this service."
			"  Instead of a reference, you can use an immediate element"
			" of some registered core.",
			forceType=core.Core, copyable=True, aliases=list(core.CORE_REGISTRY.keys()))
	
	def _makeChild(self, name, parent):
		return core.getCore(name)(parent)


@functools.lru_cache(1)
def getDALIServiceKeys():
	"""returns a list of the service keys defined in //pql#DALIPars.

	This is always the same object, so if you really have to change anything
	in here, be sure to make copies before touching either the list or
	the items.
	"""
	nullSvc = base.parseFromString(Service,
		"""<service><nullCore/><FEED source="//pql#DALIPars"/></service>""")

	res = []
	for inputKey in nullSvc.serviceKeys:
		# let's orphan them in a way that they won't be reparented
		inputKey.parent_ = base.NotGiven
		res.append(inputKey)

	return res


class GetURLMixin:
	"""A mixin for classes with rd and id, letting them compute
	URLs working with the DaCHS root.py URL dispatcher.
	"""
	def getURL(self, rendName, absolute=True, canonical=False, **kwargs):
		"""returns the full canonical access URL of this service together
		with renderer.

		rendName is the name of the intended renderer in the registry
		of renderers.

		With absolute, a fully qualified URL is being returned.

		Further keyword arguments are translated into URL parameters in the
		query part.
		"""
		basePath = "%s%s/%s"%(base.getConfig("web", "nevowRoot"),
			self.rd.sourceId, self.id)
		if absolute:
			basePath = base.makeAbsoluteURL(basePath, canonical=canonical)
		res = renderers.getRenderer(rendName
			).makeAccessURL(basePath)

		if kwargs:
			res = res+"?"+urllib.parse.urlencode(kwargs)
		return res


class Service(base.Structure, base.ComputedMetaMixin,
		base.StandardMacroMixin, rscdef.IVOMetaMixin,
		GetURLMixin):
	"""A service definition.

	A service is a combination of a core and one or more renderers.  They
	can be published, and they carry the metadata published into the VO.


	Properties evaluated:

	* votableRespectsOutputTable -- usually, VOTable output puts in
	  all columns from the underlying database table with low enough
	  verbLevel (essentially).  When this property is "True" (case-sensitive),
	  that's not done and only the service's output table is evaluated.
	* fixedFormat -- if this is set to "True", the form renderer will
	  not produce an output format selector (and we shouldn't produce
	  DALI RESPONSEFORMAT metadata).
	* publishedRenderers -- when you publish an entire RD, use these
	  (comma-separated) renderers to create capabilities from this service
	  rather than what's in allowed.  Set it to empty to hide the service
	  entirely.
	* defaultSort -- this is a name of a database column and preselects a sort
	  order.  Note again that this will slow down responses for all but the
	  smallest tables unless there is an index on that column.
	"""
	name_ = "service"

	_core = CoreAttribute()
	_templates = base.DictAttribute("templates", description="Custom"
		' nevow templates for this service; use key "form" to replace the Form'
		" renderer's standard template; qp uses resulttable and resultline"
		" depending on whether there's many result lines or just one."
		"  Start the path with two slashes to access system templates.",
		aliases=["template"],
		itemAttD=rscdef.ResdirRelativeAttribute(
			"template", description="resdir-relative path to a nevow template"
			" used for the function given in key."), copyable=True)
	_publications = base.StructListAttribute("publications",
		childFactory=Publication, description="Sets and renderers this service"
			" is published with.")
	_limitTo = base.UnicodeAttribute("limitTo", default=None,
		description="Limit access to the group given; the empty default disables"
		" access control.", copyable="True")
	_customPage = rscdef.ResdirRelativeAttribute("customPage", default=None,
		description="resdir-relative path to custom page code.  It is used"
		" by the 'custom' renderer", copyable="True")
	_allowedRenderers = base.StringSetAttribute("allowed",
		description="Names of renderers allowed on this service; leave empty"
		" to allow the form renderer only.", copyable=True)
	_customRF = base.StructListAttribute("customRFs",
		description="Custom render functions for use in custom templates.",
		childFactory=CustomRF, copyable=True)
	_customDF = base.StructListAttribute("customDFs",
		description="Custom data functions for use in custom templates.",
		childFactory=CustomDF, copyable=True)
	_outputTable = base.StructAttribute("outputTable", default=None,
		childFactory=outputdef.OutputTableDef, copyable=True, description=
		"The output fields of this service.")
	_serviceKeys = base.UniquedStructListAttribute("serviceKeys",
		uniqueAttribute="name", policy="drop",
		childFactory=inputdef.InputKey, description="Input widgets for"
			" processing by the service, e.g. output sets.", copyable=True)
	_defaultRenderer = base.UnicodeAttribute("defaultRenderer",
		default=None, description="A name of a renderer used when"
		" none is provided in the URL (lets you have shorter URLs).")

	_rd = rscdef.RDAttribute()
	_props = base.PropertyAttribute()
	_original = base.OriginalAttribute()

	metaModel = ("title(1), creationDate(1), description(1),"
		" subject, referenceURL(1), shortName(!),"
		" productTypeServed(? voc:product-type),"
		" dataSource(? voc:data-source),"
		" contentLevel(? voc:voresource/content_level)")

	# formats that should query the same fields as HTML (the others behave
	# like VOTables and offer a "verbosity" widget in forms).
	htmlLikeFormats = ["HTML", "tar"]

	####################### Housekeeping methods

	def __repr__(self):
		return "<Service %s at %s>"%(self.id, self.getSourcePosition())

	def completeElement(self, ctx):
		super().completeElement(ctx)
		if not self.allowed:
			self.allowed.add("form")

		if self.core is base.Undefined:
			raise base.StructureError("Services must have cores (add <nullCore/>"
				" if you really do not want a core, e.g., with fixed renderers).")

		# if there's only one renderer on this service, make it the default
		if self.defaultRenderer is None and len(self.allowed)==1:
			self.defaultRenderer = list(self.allowed)[0]

		# if there's a DALI-compliant renderer on this service, declare
		# its service parameters (MAXREC, RESPONSEFORMAT, etc).
		# Of course, other renderers might ignore them, but presumably
		# the metadata generated from this are only really evaluated
		# by VO/DALI components.
		# "DALI-complicance" is currently determined by a parameter
		# style of dali or pql.
		for rendName in self.allowed:
			try:
				ps = renderers.getRenderer(rendName).parameterStyle
			except base.NotFoundError:
				# letting this exc through could lead to confusing error messages
				# on cross-RD reference, so we just warn:
				base.ui.notifyWarning("Invalid renderer spec '%s' on service at %s"%
					(rendName, self.getSourcePosition()))
				continue

			if ps in ["dali", "pql"]:
				for inputKey in getDALIServiceKeys():
					self._serviceKeys.addStruct(self, inputKey)
				break

		# cache all kinds of things expensive to create and parse
		self._coresCache = {}
		self._loadedTemplates = {}
		
		# Schedule the capabilities to be added when the parse is
		# done (i.e., the RD is complete)
		ctx.addExitFunc(lambda rd, ctx: self._addAutomaticCapabilities())
			
	def onElementComplete(self):
		super().onElementComplete()
		
		# Index custom render/data functions
		self.nevowRenderers = {}
		for customRF in self.customRFs:
			self.nevowRenderers[customRF.name] = customRF.func
		self.nevowDataFunctions = {}
		for customDF in self.customDFs:
			self.nevowDataFunctions[customDF.name] = customDF.func

		self._compileCustomPage()

		self._computeResourceType()

		if hasattr(self.core, "queriedTable"):
			# Copy over any served-by relationships from our table if
			# we query one.  This is important, for instance, for
			# obscore-published tables, where we want to let our
			# clients know they could be using obscore instead.
			for m in self.core.queriedTable.iterMeta("isServedBy"):
				self.addMeta("isServedBy", m)


	def _compileCustomPage(self):
		if self.customPage:
			try:
				modNs, moddesc = utils.loadPythonModule(self.customPage)
				modNs.RD = self.rd
				getattr(modNs, "initModule", lambda: None)()
				page = modNs.MainPage
			except ImportError:
				raise base.ui.logOldExc(
					base.LiteralParseError("customPage", self.customPage,
					hint="This means that an exception was raised while DaCHS"
						" tried to import the renderer module.  If DaCHS ran"
						" with --debug, the original traceback is available"
						" in the logs."))
			self.customPageCode = page, (os.path.basename(self.customPage),moddesc)

	def getTemplate(self, key, reload=False):
		"""returns the nevow template for the function key on this service.

		Pass reload to force a reload of templates already parsed.
		"""
		if reload or key not in self._loadedTemplates:
			tp = self.templates[key]
			if tp.startswith("//"):
				self._loadedTemplates[key] = common.loadSystemTemplate(tp[2:])
			else:
				self._loadedTemplates[key] = nevowc.XMLFile(
					os.path.join(self.rd.resdir, tp))
		return self._loadedTemplates[key]

	def getUWS(self):
		"""returns the UWS worker system for this service.

		This is a service for the DALIAsyncRenderer.
		"""
		if not hasattr(self, "uws"):
			if hasattr(self.core, "workerSystem"):
				self.uws = self.core.workerSystem
			else:
				# todo: we don't want that any more.  People wanting user UWS
				# should attach their worker systems to their cores in the future.
				from gavo.protocols import useruws
				self.uws = useruws.makeUWSForService(self)
		return self.uws

	################### Registry and related methods.

	@property
	def isVOPublished(self, renderer=None):
		"""is true if there is any ivo_managed publication on this
		service.

		If renderer is non-None, only publications with this renderer name
		count.
		"""
		for pub in self.publications:
			if "ivo_managed" in pub.sets:
				if renderer:
					if pub.render==renderer:
						return True
				else:
					return True
		return False

	def _computeResourceType(self):
		"""sets the resType attribute.

		Services are resources, and the registry code wants to know what kind.
		This method ventures a guess.  You can override this decision by setting
		the resType meta item.
		"""
		if (self.core.outputTable.columns
			or "dali" in self.allowed):
			self.resType = "catalogService"
		else: # no output table defined, we're a plain service
			self.resType = "dataService"

	def _iterAutomaticCapabilities(self):
		"""helps _addAutomaticCapabilities.

		Actually, we also use it to generate VOSI capabilities for
		unpublished services.
		"""
		vosiSet = set(["ivo_managed"])

		# Let's give everything a capability, even null cores.
		yield base.makeStruct(Publication,
				render="capabilities",
				sets=vosiSet,
				parent_=self)

		# If it has a core, there is tables involved, and we do VOSI tables, too
		if not isinstance(self.core, core.getCore("nullCore")):
			yield base.makeStruct(Publication,
					render="availability",
					sets=vosiSet,
					parent_=self)
			yield base.makeStruct(Publication,
					render="tableMetadata",
					sets=vosiSet,
					parent_=self)

		# things querying tables get a TAP relationship if
		# their table is adql-queriable
		if isinstance(self.core, core.getCore("dbCore")):
			if self.core.queriedTable.adql:
				tapService = base.resolveCrossId("//tap#run")
				yield base.makeStruct(Publication,
						render="dali",
						sets=vosiSet,
						auxiliary=True,
						service=tapService,
						parent_=self)

		# things with examples meta get an examples capability
		try:
			self.getMeta("_example", raiseOnFail=True)
			yield base.makeStruct(Publication,
					render="examples",
					sets=utils.AllEncompassingSet(),
					parent_=self)
		except base.NoMetaKey:
			pass

	def _addAutomaticCapabilities(self):
		"""adds some publications that are automatic for certain types
		of services.

		For services with ivo_managed publications and with useful cores
		(this keeps out doc-like publications, which shouldn't have VOSI
		resources), artificial VOSI publications are added.

		If there is _example meta, an examples publication is added.

		If this service exposes a table (i.e., a DbCore with a queriedTable)
		and that table is adql-readable, also add an auxiliary TAP publication
		if going to the VO.

		This is being run as an exit function from the parse context as
		we want the RD to be complete at this point (e.g., _examples
		meta might come from it).  This also lets us liberally resolve
		references anywhere.
		"""
		if not self.isVOPublished:
			return

		for pub in self._iterAutomaticCapabilities():
			self._publications.feedObject(self, pub)

	  # This is no longer a good place to do this, but TAP-published
		# tables need a isServedBy, and we since it's automatic, let's
		# do it here.
		# According to the "discovering dependent" note, we don't
		# do the reverse relationship lest the TAP service
		# gets too related...
		if isinstance(self.core, core.getCore("dbCore")):
			if self.core.queriedTable.adql:
				tapService = base.resolveCrossId("//tap#run")
				self.addMeta("isServedBy",
					base.getMetaText(tapService, "title"),
					ivoId=base.getMetaText(tapService, "identifier"))

	def _iterAssociatedDatalinkServices(self):
		"""yields instances for datalink services appropriate for this
		service.

		The datalink services are taken from the core's queriedTable
		attribute, if available.  As a legacy fallback, for now
		the datalink property on the service is supported as well for now,
		but I'll try to get rid of that.
		"""
		queriedTable = getattr(self.core, "queriedTable", None)
		# if there's not queried table, there's not much point in
		# having a datalink service.
		linkGenerated = False
		if queriedTable:
			for svcRef in queriedTable.iterMeta(
					"_associatedDatalinkService.serviceId"):
				yield base.resolveId(self.rd, str(svcRef))
				linkGenerated = True

		# Legacy fallback in case no datalink services are found in the table:
		# the datalink property
		if not linkGenerated:
			svcRef = self.getProperty("datalink", None)
			if svcRef:
				yield self.rd.getById(svcRef)

	def getPublicationsForSet(self, names, includeDatalink=True):
		"""returns publications for a set of set names (the names argument).

		In the special case names=None, all allowed renderers are treated
		as published.
		"""
		addAllAllowed = False
		if names is None:
			names = utils.AllEncompassingSet()
			addAllAllowed = True

		# the or in the list comprehension is because I can't see a way
		# to make AllEmcompassingSet work on the right side of the
		# operand, but it can occur on both sides.
		result = [pub for pub in self.publications
			if pub.sets & names or names & pub.sets]

		# for ivo_managed, also return a datalink endpoints if they're
		# there; the specs imply that might be useful some day.
		# (btw., I think we shouldn't do that but instead extend
		# publish so people can ask for other services to be included
		# as capabilities).
		if includeDatalink and "ivo_managed" in names:
			for dlSvc in self._iterAssociatedDatalinkServices():
				if "dlget" in dlSvc.allowed:
					result.append(base.makeStruct(Publication,
						render="dlget",
						sets=set(["ivo_managed"]),
						service=dlSvc))

				if "dlasync" in dlSvc.allowed:
					result.append(base.makeStruct(Publication,
						render="dlasync",
						sets=set(["ivo_managed"]),
						service=dlSvc))

				if "dlmeta" in dlSvc.allowed:
					result.append(base.makeStruct(Publication,
						render="dlmeta",
						sets=set(["ivo_managed"]),
						service=dlSvc))

		if addAllAllowed: # name=None was passed in,
			# add publications for everything allowed but not yet marked
			# as published (for VOSI)
			alreadyPublished = set(p.render for p in result
				# some TAP subrenderers never get capabilities for now
				# (but then all this needs to change anyway; cf. caproles)
				) | frozenset(["sync", "async", "user_tables"])

			for rendName in self.allowed:
				if not rendName in alreadyPublished:
					result.append(base.makeStruct(Publication,
						render=rendName, sets=set(["vosi"]), service=self))
			# these will only be added here for unpublished services
			for pub in self._iterAutomaticCapabilities():
				if not pub.render in alreadyPublished:
					result.append(pub)

		return result

	# used by getBrowserURL; keep external higher than form as long as
	# we have mess like Potsdam CdC.
	_browserScores = {"form": 10, "external": 12, "fixed": 15,
		"custom": 8, "img.jpeg": 7, "static": 5, "hips": 3, "examples": 2}

	def getBrowserURL(self, fq=True):
		"""returns a published URL that's suitable for a web browser or None if
		no such URL can be guessed.

		If you pass fq=False, you will get a path rather than a URL.
		"""
		# There can be multiple candidates for browser URLs (like when a service
		# has both form, static, and external renderers).  If so, we select
		# by plain scores.
		browseables = []
		for rendName in self.allowed:
			if self.isBrowseableWith(rendName):
				browseables.append((self._browserScores.get(rendName, -1), rendName))
		if browseables:
			return self.getURL(max(browseables)[1], absolute=fq)
		else:
			return None
		
	def isBrowseableWith(self, rendName):
		"""returns true if rendering this service through rendName results
		in something pretty in a web browser.
		"""
		try:
			return bool(renderers.getRenderer(rendName).isBrowseable(self))
		except base.NotFoundError: # renderer name not known
			return False

	def getTableSet(self):
		"""returns a list of table definitions that have something to do with
		this service.

		This is for VOSI-type requests.

		Basically, we're leaving the decision to the core, except when
		we have an output Table of our own.
		"""
		if self.outputTable and self.outputTable.columns:
			return [self.outputTable]

		return self.core.getRelevantTables()

	def declareServes(self, data):
		"""adds meta to self and data indicating that data is served by
		this service.

		This is used by table/@adql and the publish element on data.
		"""
		if data.registration:
			self.addMeta("isServiceFor",
				base.getMetaText(data, "title", default="Anonymous"),
				ivoId=base.getMetaText(data, "identifier"))
			data.addMeta("isServedBy",
				base.getMetaText(self, "title"),
				ivoId=base.getMetaText(self, "identifier"))

			# Since this is always initiated by the data, the dependency
			# must show up in its RD to be properly added on publication
			# and to be removed when the data is removed.
			data.rd.addDependency(self.rd, data.rd)

	########################## Output field selection

	_allSet = set(["ALL"])

	def _getFilteredColumns(self, columnSource, queryMeta, verbLevel):
		"""filters columns in columnSource according to verbosity and column
		set given in queryMeta.

		Actually, we only evaluate verbosity and requireSet.
		"""
		if queryMeta["columnSet"]:
			columnSource = [f for f in columnSource
				if f.sets==self._allSet or queryMeta["columnSet"]&f.sets]

		return [f for f in columnSource
			if f.verbLevel<=verbLevel and not f.hidden]

	def _getVOTableOutputFields(self, queryMeta):
		"""returns a list of OutputFields suitable for a VOTable
		response described by queryMeta.

		This is the set of all columns in the source table below
		the verbosity defined in queryMeta, except that columns
		with a displayHint of noxml present are thrown out, too.

		When the service sets the votableRespectsOutputTable property to
		"True", the column source is the service's output table rather
		than the core's one.
		"""
		verbLevel = queryMeta.get("verbosity")
		if verbLevel=="HTML":
			# SAMP transfers, typically: Pretend we're HTML
			columnSource = self.getHTMLOutputFields(queryMeta)
			verbLevel = 100 # column selection done by HTML, just filter noxml below

		elif (self.getProperty("votableRespectsOutputTable", "").lower()=="true"
				or queryMeta["columnSet"]):
			columnSource = self.outputTable

		else:
			columnSource = self.getAllOutputFields()

		fields = self._getFilteredColumns(
			[f for f in columnSource
				if f.displayHint.get("noxml")!="true"],
			queryMeta,
			verbLevel)

		return rscdef.ColumnList(fields)

	def getHTMLOutputFields(self, queryMeta, ignoreAdditionals=False,
			raiseOnUnknown=True):
		"""returns a list of OutputFields suitable for an HTML response described
		by queryMeta.

		This is the service's output table if given, else the core's output
		table at verbLevel 2.  Additional fields can be set by the user.

		raiseOnUnknown is used by customwidgets to avoid exceptions because of
		bad additional fields during form construction (when they aren't
		properly caught).
		"""
		if self.outputTable:
			columnSource, verbLevel = self.outputTable.columns, 20
		else:
			columnSource, verbLevel = self.core.outputTable.columns, 20

		if queryMeta["verbosity"]!='HTML':
			verbLevel = queryMeta["verbosity"]

		fields = self._getFilteredColumns(columnSource, queryMeta, verbLevel)

		# add user-selected fields
		if not ignoreAdditionals and queryMeta["additionalFields"]:
			try:
				for fieldName in queryMeta["additionalFields"]:
					col = self.core.outputTable.getColumnByName(fieldName)
					if isinstance(col, outputdef.OutputField):
						fields.append(col)
					else:
						fields.append(outputdef.OutputField.fromColumn(col))
			except base.NotFoundError as msg:
				if raiseOnUnknown:
					raise base.ValidationError("The additional field %s you requested"
						" does not exist"%repr(msg.lookedFor), colName="_OUTPUT")

		return rscdef.ColumnList(fields)

	def getCurOutputFields(self, queryMeta=None, raiseOnUnknown=True):
		"""returns a list of desired output fields for query meta.

		This is for both the core and the formatter to figure out the
		structure of the tables passed.
		"""
		queryMeta = queryMeta or common.emptyQueryMeta

		# "renderer" in queryMeta needs to be set in the run method below;
		# the net effect is that format will come from RESPONSEFORMAT, _FORMAT,
		# and the renderer default in sequence.  If none of that is available,
		# blindly go for default HTML (which in retrospect perhaps wasn't the
		# best choice for DaCHS, but changing this now to VOTable would introduce
		# needless breakage).
		renderer = queryMeta.get("renderer")
		format = queryMeta.get("format",
			getattr(renderer, "defaultOutputFormat", None)) or "HTML"

		if format in self.htmlLikeFormats:
			return self.getHTMLOutputFields(queryMeta, raiseOnUnknown=raiseOnUnknown)
		else:
			return self._getVOTableOutputFields(queryMeta)

	def getAllOutputFields(self):
		"""Returns a sequence of all available output fields.

		This is what the core gives, and this is what will be declared
		to the registry.  Depending on the output format, the verbosity
		level and perhaps other user settings, the actual columns produced
		will be different.
		"""
		return self.core.outputTable.columns

	################### running and input computation.

	def getCoreFor(self, renderer, queryMeta):
		"""returns a core tailored for renderer.

		See svcs.core's module docstring.
		
		The argument can be a renderer or a renderer name.
		"""
		if isinstance(renderer, str):
			renderer = renderers.getRenderer(renderer)

		# non-checked renderers use the core for info purposes only; don't
		# bother for those
		if not renderer.checkedRenderer:
			return self.core

		if renderer.name not in self._coresCache:
			# Bad Hack: Tell datalink core what renderers are allowed on
			# this service
			allowedRendsForStealing = self.allowed #noflake: for stealVar downstack

			res = self.core.adaptForRenderer(renderer, queryMeta)

			# Hack: let the polymorphous datalink core suppress caching
			if getattr(res, "nocache", False):
				return res

			self._coresCache[renderer.name] = res
		return self._coresCache[renderer.name]

	def getContextGrammarFor(self, renderer, queryMeta, core=None):
		"""returns an ContextGrammar appropriate for this renderer.

		Pass in the core if you already have it as an optimisation (in
		particular for datalink, where cores aren't automatically cached);
		if you don't the core will be computed from the renderer.

		In either case, the context grammar simply is built from the core's
		inputTable.
		"""
		if isinstance(renderer, str):
			renderer = renderers.getRenderer(renderer)
		if core is None:
			core = self.getCoreFor(renderer, queryMeta)

		serviceKeys = list(inputdef.filterInputKeys(self.serviceKeys,
			renderer.name, inputdef.getRendererAdaptor(renderer)))

		return MS(inputdef.ContextGrammar,
				inputTD=core.inputTable,
				inputKeys=serviceKeys)

	def getInputKeysFor(self, renderer, queryMeta=common.emptyQueryMeta):
		"""returns a sequence of input keys, adapted for renderer.

		The renderer argument may either be a renderer name, a renderer
		class or a renderer instance.

		This is the main interface for external entities to discover.
		service metadata.
		"""
		if isinstance(renderer, str):
			renderer = renderers.getRenderer(renderer)
		return list(self.getContextGrammarFor(
			renderer, queryMeta).iterInputKeys())

	def adaptCoreRes(self, coreRes, queryMeta):
		"""adapts a core result to the service's output table if appropriate.

		Frankly, this was an early mis-design that complicates matters
		unnecessarily, but I can't really drop it as long as some interesting
		things are based on service's outputTables.

		The adaptation works like this:

		(0) if coreRes isn't a table, or that table has a noPostprocess
		attribute, return coreRes and let the renderer worry about it.

		(1) if names and units of newColumns the same as coreRes.columns,
		accept coreRes as the new table

		(2) if names of newColumns are a subset of coreRes.columns match
		but one or more units don't, set up a conversion routine and create
		new rows, combining them with newColumns to the result.

		(3) else raise an error

		(4) eventually, wrap everything up in a rsc.Data instance for
		compatibility with cores actually returning data items.
		"""
		if not isinstance(coreRes, rsc.BaseTable):
			return coreRes

		newTable = coreRes
		if "requestURI" in queryMeta:
			newTable.setMeta("request_uri", queryMeta["requestURI"])

		if hasattr(coreRes, "noPostprocess"):
			colDiffs, swallowColumns = None, None
		else:
			newColumns = self.getCurOutputFields(queryMeta)
			swallowColumns = {c.name for c in coreRes.tableDef
				}-{c.name for c in newColumns}
			colDiffs = base.computeColumnConversions(
				newColumns, coreRes.tableDef.columns)

		if colDiffs or swallowColumns:
			newTd = coreRes.tableDef.change(columns=newColumns)
			newTd.copyMetaFrom(coreRes.tableDef)

			if colDiffs:
				rmk = rscdef.RowmakerDef(None)
				for col in newColumns:
					exprStart = ""
					if col.name in colDiffs:
						exprStart = "%s*"%colDiffs[col.name]
					rmk.feedObject("map", rmkdef.MapRule(rmk, dest=col.name,
						content_="%svars[%s]"%(exprStart, repr(col.name))
						).finishElement(None))
				newTable = table.InMemoryTable(newTd, validate=False)
				mapper = rmk.finishElement(None).compileForTableDef(newTd)
				for r in coreRes:
					newTable.addRow(mapper(r, newTable))

			else:
				newTable = rsc.TableForDef(newTd, rows=coreRes.rows)

			newTable._params = coreRes._params

		resData = rsc.wrapTable(newTable, rdSource=coreRes.tableDef)

		return resData

	def _runWithInputTable(self, core, inputTable, queryMeta):
		"""runs the core and returns a service result.
		"""
		queryMeta["inputTable"] = inputTable
		coreRes = core.run(self, inputTable, queryMeta)
		res = self.adaptCoreRes(coreRes, queryMeta)

		return res

	def run(self, renderer, args, queryMeta):
		"""runs the service, returning a service result.

		This is the main entry point for protocol renderers; args is
		a dict of lists as provided by request.strargs.
		"""
		if isinstance(renderer, str):
			renderer = renderers.getRenderer(renderer)

		core = self.getCoreFor(renderer, queryMeta)
		coreArgs = inputdef.CoreArgs.fromRawArgs(
			core.inputTable, args, self.getContextGrammarFor(
				renderer, queryMeta, core))
		queryMeta["renderer"] = renderer
	
		return self._runWithInputTable(core, coreArgs, queryMeta)


	#################### meta and such

	def _meta_available(self):
# XXX TODO: have this ask the core
		return "true"

	def _meta_sets(self):
		# this is the union of the sets of the publications; this
		# is a bit lame, but it's a consequence of our mismatch
		# between publications, capabilities, and services.
		# In practice, these sets are meaningless where they are
		# pulled from meta (everywhere except in service selection
		# and in list identifiers, both of which don't use this).
		# There's a copy of this in registry.nonservice.
		sets = set()
		for p in self.publications:
			sets |= p.sets
		if sets:
			return meta.MetaItem.fromSequence(
				[meta.MetaValue(s) for s in sets])
		else:
			return None

	def macro_tablesForTAP(self):
		"""returns a list of table names available for TAP querying.

		This, really, is an implementation detail for the TAP service and
		might go away anytime.
		"""
		# this is only used by tap.rd -- maybe it
		# should go there?
		from gavo.protocols import tap
		
		schemas = {}
		for qname in tap.getAccessibleTables():
			try:
				schema, name = qname.split(".")
			except: # weird name
				continue
			schemas.setdefault(schema, []).append(name)

		return ", ".join("%s from the %s schema"%(", ".join(tables), schema)
			for schema, tables in schemas.items())
	
	def _meta_examplesLink(self):
		"""returns a link to a examples for this service if any
		are available.
		"""
		try:
			self.getMeta("_example", raiseOnFail=True)
			return base.META_CLASSES_FOR_KEYS["_related"](
				self.getURL("examples", False),
				title="DALI examples")
		except base.NoMetaKey:
			return None
	
	def _meta_howtociteLink(self):
		"""returns a link to a how-to-cite page for this service as an URL
		meta.
		"""
		if self.getMeta("creationDate"):
			# depend on creationDate because to filter out totally whacko
			# RDs that will never make it to the Registry.
			return base.META_CLASSES_FOR_KEYS["_related"](
				self.getURL("howtocite", absolute=True),
				title="Advice on citing this resource")
