#! /usr/bin/env python3
##############################################################################
#
# Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved.
#                    Yoshinori Okuji <yo@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################


"""Generate repository information on Business Templates.
"""

import posixpath
import tarfile
import os
import sys
try:
  from html import escape
except ImportError:
  from cgi import escape # Deprecated since version 3.2
from base64 import b64encode
from io import BytesIO
from hashlib import sha1
try:
  from urllib.parse import unquote
except ImportError:
  from urllib import unquote

if sys.version_info[0] == 3:
  def iteritems(d):
    return iter(d.items())
else:
  def iteritems(d):
    return d.iteritems()

# Order is important for installation
# We want to have:
#  * workflow and portal_type* before ZODB Component {Document,Extension...}
#  * path after module, because path can be module content
#  * path after categories, because path can be categories content
#  * path after portal types roles so that roles in the current bt can be used
#  * path before workflow chain, because path can be a portal type
#         (until chains are set on portal types with categories)
#  * skin after paths, because we can install a custom connection string as
#       path and use it with SQLMethods in a skin.
#    ( and more )
item_name_list = (
  'registered_version_priority_selection',
  'workflow',
  'product',
  'module_component',
  'document',
  'interface',
  'mixin',
  'tool_component',
  'property_sheet',
  'constraint',
  'extension',
  'test',
  'role',
  'tool',
  'message_translation',
  'site_property',
  'portal_type',
  'portal_type_allowed_content_type',
  'portal_type_hidden_content_type',
  'portal_type_property_sheet',
  'portal_type_base_category',
  'portal_type_type_mixin',
  'category',
  'module',
  'portal_type_roles',
  'path',
  'skin',
  'registered_skin_selection',
  'preference',
  'action',
  'local_roles',
  'portal_type_workflow_chain',
  'catalog_method',
  'catalog_result_key',
  'catalog_related_key',
  'catalog_result_table',
  'catalog_search_key',
  'catalog_keyword_key',
  'catalog_datetime_key',
  'catalog_full_text_key',
  'catalog_request_key',
  'catalog_multivalue_key',
  'catalog_topic_key',
  'catalog_scriptable_key',
  'catalog_role_key',
  'catalog_local_role_key',
  'catalog_security_uid_column',
)

item_set = set(('CatalogDateTimeKey' if x == 'catalog_datetime_key' else
             ''.join(map(str.title, x.split('_')))) + 'TemplateItem'
            for x in item_name_list)
item_set.add('bt')
item_name_list = tuple('_%s_item' % x for x in item_name_list)

class BusinessTemplateRevision(list):

  def hash(self, path, text):
    self.append((path.encode('utf-8'), sha1(text).digest()))

  def digest(self):
    self.sort()
    return b64encode(sha1(b'\0'.join(h + p for (h, p) in self)).digest())


class BusinessTemplate(dict):

  property_list = frozenset('''
title
version
description
license
dependency_list
test_dependency_list
provision_list
copyright_list
force_install
'''.split())

  def __init__(self):
    self.revision = BusinessTemplateRevision()

  def _read(self, path, file):
    try:
      text = file.read()
    finally:
      file.close()
    if path.startswith('bt/'):
      name = path[3:]
      if name in self.property_list:
        if name.endswith('_list'):
          self[name[:-5]] = text.splitlines()
        else:
          self[name] = text
      elif name == 'revision':
        return
    self.revision.hash(unquote(path) if '%' in path else path, text)

  def __iter__(self):
    self['revision'] = self.revision.digest()
    return iter(sorted(iteritems(self)))

  @classmethod
  def fromTar(cls, tar):
    """Read an archived Business Template info"""
    self = cls()
    for info in tar:
      if not info.isdir():
        name = info.name.split('/', 1)[1]
        if name.split('/', 1)[0] in item_set:
          self._read(name, tar.extractfile(info))
    return iter(self)

  @classmethod
  def fromDir(cls, dir):
    """Read Business Template Directory info"""
    self = cls()
    lstrip_len = len(dir + os.sep)
    for root, dirs, files in os.walk(dir):
      if root:
        for path in files:
          path = os.path.join(root, path)
          self._read(posixpath.normpath(path[lstrip_len:]), open(path, 'rb'))
      else:
        dirs[:] = item_set.intersection(dirs)
    return iter(self)

def generateInformation(dir, info=id, err=None):
  xml = BytesIO()
  xml.write(b'<?xml version="1.0"?>\n<repository>\n')
  for name in sorted(os.listdir(dir)):
    path = os.path.join(dir, name)
    if name.endswith('.bt5'):
      info('Reading %s... ' % name)
      try:
        tar = tarfile.open(path, 'r:gz')
      except tarfile.TarError:
        if err:
          err('An error happened in %s; skipping\n' % name)
          continue
        raise
      try:
        property_list = BusinessTemplate.fromTar(tar)
      finally:
        tar.close()
    elif os.path.isfile(os.path.join(path, 'bt', 'title')):
      info('Reading Directory %s... ' % name)
      property_list = BusinessTemplate.fromDir(path)
    else:
      continue
    xml.write(b'  <template id="%s">\n' % name.encode())
    for k, v in property_list:
      if str is not bytes:
        k = k.encode()
      for v in (v,) if type(v) is bytes else v:
        xml.write(b'    <%s>%s</%s>\n' % (k, escape(v, quote=False) if str is bytes else
                                             escape(v.decode(), quote=False).encode(), k))
    xml.write(b'  </template>\n')
    info('done\n')
  xml.write(b'</repository>\n')
  return xml

def main(dir_list=None, **kw):
  if dir_list is None:
    kw.setdefault('info', sys.stdout.write)
    kw.setdefault('err', sys.stderr.write)
    dir_list = sys.argv[1:] or '.'

  for d in dir_list:
    bt5list = generateInformation(d, **kw).getvalue()
    # add pid in filename to avoid conflicts if several process calls genbt5list
    destination_path =  os.path.join(d, 'bt5list')
    temporary_path = destination_path + '.new.%i' % os.getpid()
    try:
      with open(temporary_path, 'wb') as f:
        f.write(bt5list)
      os.rename(temporary_path, destination_path)
    finally:
      try:
        os.remove(temporary_path)
      except OSError:
        pass

if __name__ == "__main__":
  main()