Repository: scieloorg/catalogmanager

Issue:
Armazenar o XML sem espaços entre tags (minificado).
Devemos testar a hipótese de que: cada caractere em branco (espaço) no UTF-8 ocupa 1 byte, o que pode se tornar dispendioso em termos de consumo de disco e tráfego de rede, se levarmos em conta o volume de dados do sistema.

Comments:

Patch:
diff --git a/managers/__init__.py b/managers/__init__.py
index 38728b9..6cac9ea 100644
--- a/managers/__init__.py
+++ b/managers/__init__.py
@@ -138,8 +138,8 @@ def set_assets_public_url(article_id, xml_content, assets_filenames,
 
     :returns: conteúdo do XML atualizado
     """
-    article = ArticleDocument(article_id)
-    article.xml_file = File(file_name="xml_file.xml", content=xml_content)
+    xml_file = File(file_name="xml_file.xml", content=xml_content)
+    article = ArticleDocument(article_id, xml_file)
     for name in article.assets:
         if name in assets_filenames:
             article.assets[name].href = public_url.format(article_id,
diff --git a/managers/article_manager.py b/managers/article_manager.py
index 3baa212..7c58fa4 100644
--- a/managers/article_manager.py
+++ b/managers/article_manager.py
@@ -37,8 +37,7 @@ class ArticleManager:
         return article.unexpected_files_list, article.missing_files_list
 
     def receive_xml_file(self, id, xml_file):
-        article = ArticleDocument(id)
-        article.xml_file = xml_file
+        article = ArticleDocument(id, xml_file)
 
         article_record = Record(
             document_id=article.id,
@@ -83,18 +82,17 @@ class ArticleManager:
 
     def get_article_file(self, article_id):
         article_record = self.get_article_data(article_id)
-        article = ArticleDocument(article_id)
         try:
             attachment = self.article_db_service.get_attachment(
                 document_id=article_id,
                 file_id=article_record['content']['xml']
             )
-            article.xml_file = File(file_name=article_record['content']['xml'],
-                                    content=attachment)
-            return article.xml_file.content
+            xml_file = File(file_name=article_record['content']['xml'],
+                            content=attachment)
+            return xml_file.content
         except DocumentNotFound:
             raise ArticleManagerException(
-                'Missing XML file {}'.format(article_id)
+                'XML file {} not found'.format(article_id)
             )
 
     def get_asset_files(self, article_id):
diff --git a/managers/models/article_model.py b/managers/models/article_model.py
index 5ff3847..7d9d8ba 100644
--- a/managers/models/article_model.py
+++ b/managers/models/article_model.py
@@ -5,7 +5,6 @@ from ..xml.article_xml_tree import ArticleXMLTree
 
 class AssetDocument:
     """Metadados de um documento do tipo Ativo Digital.
-    
     Um Ativo Digital é um arquivo associado a um documento do tipo Artigo
     por meio de uma referência interna na estrutura da sua representação em
     XML.
@@ -18,8 +17,6 @@ class AssetDocument:
         #gerenciado pela instância mas pelo seu cliente.
         self.file = None
         self.node = asset_node
-        #XXX :attr:`.name` e :attr:`.href` perdem integridade após atribuição
-        #em :attr:`.href`.
         self.name = asset_node.href
 
     @property
@@ -39,21 +36,18 @@ class AssetDocument:
 class ArticleDocument:
     """Metadados de um documento do tipo Artigo.
 
-    Os metadados contam com uma referência ao Artigo codificado em XML e 
+    Os metadados contam com uma referência ao Artigo codificado em XML e
     referências aos seus ativos digitais.
 
     Exemplo de uso:
 
-        #XXX note que a inicialização da instância não é feita por completo no
-        #momento devido.
-
-        >>> doc = ArticleDocument('art01')
-        >>> doc.xml_file = <instância de File>  
+        >>> doc = ArticleDocument('art01', <instância de File>)
     """
-    def __init__(self, article_id):
+    def __init__(self, article_id, xml_file):
         self.id = article_id
         self.assets = {}
         self.unexpected_files_list = []
+        self.xml_file = xml_file
 
     @property
     def xml_file(self):
@@ -62,7 +56,7 @@ class ArticleDocument:
         um novo documento Artigo resultará na identificação dos seus ativos,
         i.e., o valor do atributo :attr:`.assets` será modificado.
 
-        Adicionalmente, a definição de um novo documento Artigo causará a 
+        Adicionalmente, a definição de um novo documento Artigo causará a
         definição do atributo :attr:`.xml_tree`.
 
         O acesso ao documento Artigo antes que este seja inicializado resultará
@@ -73,11 +67,7 @@ class ArticleDocument:
     @xml_file.setter
     def xml_file(self, xml_file):
         self._xml_file = xml_file
-        #XXX o atributo não é definido até que :meth:`xml_file` seja 
-        #executado definindo um documento Artigo, i.e., a API do objeto
-        #varia de acordo com o seu ciclo de vida.
-        self.xml_tree = ArticleXMLTree()   
-        self.xml_tree.content = self._xml_file.content
+        self.xml_tree = ArticleXMLTree(self._xml_file.content)
         self.assets = {
             name: AssetDocument(node)
             for name, node in self.xml_tree.asset_nodes.items()
@@ -99,11 +89,11 @@ class ArticleDocument:
         """Associa o ativo ``file`` aos metadados de um Artigo, sobrescrevendo
         valores associados anteriormente.
 
-        Retorna instância de :class:`AssetDocument` no caso de sucesso, ou 
-        ``None`` caso contrário. Caso o valor retornado seja ``None`` você 
+        Retorna instância de :class:`AssetDocument` no caso de sucesso, ou
+        ``None`` caso contrário. Caso o valor retornado seja ``None`` você
         poderá inspecionar o atributo :attr:`.unexpected_files_list` para
         saber se trata-se de um ativo desconhecido pelo Artigo ou se trata-se
-        de um artigo que não possui o atributo ``name``.                  
+        de um artigo que não possui o atributo ``name``.
         """
         name = file.name
         if name:
diff --git a/managers/xml/xml_tree.py b/managers/xml/xml_tree.py
index 21d0d51..a1283b2 100644
--- a/managers/xml/xml_tree.py
+++ b/managers/xml/xml_tree.py
@@ -1,7 +1,10 @@
 # coding=utf-8
 
 from lxml import etree
-from io import BytesIO
+from io import (
+    BytesIO,
+    StringIO,
+)
 
 
 namespaces = {}
@@ -15,14 +18,14 @@ for namespace_id, namespace_link in namespaces.items():
 
 class XMLTree:
 
-    def __init__(self):
+    def __init__(self, xml_content):
         self.tree = None
         self.xml_error = None
+        self.content = xml_content
 
     @property
     def content(self):
-        if self.tree is not None:
-            return etree.tostring(self.tree.getroot(), encoding='utf-8')
+        return self.otimized
 
     @content.setter
     def content(self, xml_content):
@@ -39,4 +42,28 @@ class XMLTree:
         return (r, message)
 
     def compare(self, xml_content):
-        return self.content == xml_content
+        return self.content == XMLTree(xml_content).content
+
+    @property
+    def tostring(self):
+        if self.tree is not None:
+            return etree.tostring(self.tree.getroot(), encoding='utf-8')
+
+    @property
+    def pretty(self):
+        return etree.tostring(
+            self.tree.getroot(),
+            encoding='utf-8',
+            pretty_print=True)
+
+    @property
+    def otimized(self):
+        parser = etree.XMLParser(remove_blank_text=True)
+        content = self.tostring
+        if content is not None:
+            root = etree.XML(content.decode('utf-8'), parser)
+            b = etree.tostring(root, encoding='utf-8')
+            s = b.decode('utf-8')
+            while ' '*2 in s:
+                s = s.replace(' '*2, ' ')
+            return s.encode('utf-8')


Test patch:
diff --git a/managers/tests/test_article_manager.py b/managers/tests/test_article_manager.py
index 6356300..e391079 100644
--- a/managers/tests/test_article_manager.py
+++ b/managers/tests/test_article_manager.py
@@ -148,8 +148,7 @@ def test_get_article_file(setup,
     )
     article_check = article_manager.get_article_file('ID')
     assert article_check is not None
-    xml_tree = XMLTree()
-    xml_tree.content = test_package_A[0].content
+    xml_tree = XMLTree(test_package_A[0].content)
     assert xml_tree.compare(article_check)
 
 
diff --git a/managers/tests/test_article_model.py b/managers/tests/test_article_model.py
index 93378ff..cbd8551 100644
--- a/managers/tests/test_article_model.py
+++ b/managers/tests/test_article_model.py
@@ -8,28 +8,21 @@ from managers.xml.xml_tree import (
 
 
 def test_article(test_package_A, test_packA_filenames):
-    article = ArticleDocument('ID')
-    xml_file = test_package_A[0]
-    article.xml_file = xml_file
+    article = ArticleDocument('ID', test_package_A[0])
     article.update_asset_files(test_package_A[1:])
     expected = {
         'assets': [asset for asset in test_packA_filenames[1:]],
         'xml': test_packA_filenames[0],
     }
     assert article.xml_file.name == test_packA_filenames[0]
+    assert article.xml_file.content == test_package_A[0].content
     assert article.xml_tree.xml_error is None
+    assert article.xml_tree.compare(test_package_A[0].content)
     assert article.get_record_content() == expected
-    assert article.xml_file.content == xml_file.content
-    xml_from_file = XMLTree()
-    xml_from_file.content = article.xml_file.content
-    xml_from_tree = XMLTree()
-    xml_from_tree.content = article.xml_tree.content
-    assert xml_from_file.content == xml_from_tree.content
 
 
 def test_missing_files_list(test_package_B):
-    article = ArticleDocument('ID')
-    article.xml_file = test_package_B[0]
+    article = ArticleDocument('ID', test_package_B[0])
     article.update_asset_files(test_package_B[1:])
 
     assert len(article.assets) == 3
@@ -47,8 +40,7 @@ def test_missing_files_list(test_package_B):
 
 
 def test_unexpected_files_list(test_package_C, test_packC_filenames):
-    article = ArticleDocument('ID')
-    article.xml_file = test_package_C[0]
+    article = ArticleDocument('ID', test_package_C[0])
     article.update_asset_files(test_package_C[1:])
 
     assert len(article.assets) == 2
@@ -67,8 +59,7 @@ def test_unexpected_files_list(test_package_C, test_packC_filenames):
 def test_update_href(test_package_A, test_packA_filenames):
     new_href = 'novo href'
     filename = '0034-8910-rsp-S01518-87872016050006741-gf01.jpg'
-    article = ArticleDocument('ID')
-    article.xml_file = test_package_A[0]
+    article = ArticleDocument('ID', test_package_A[0])
     article.update_asset_files(test_package_A[1:])
     content = article.xml_tree.content
     asset = article.assets.get(filename)
diff --git a/managers/tests/test_xml_tree.py b/managers/tests/test_xml_tree.py
index 9379912..e75a1f2 100644
--- a/managers/tests/test_xml_tree.py
+++ b/managers/tests/test_xml_tree.py
@@ -6,17 +6,15 @@ from managers.xml.xml_tree import (
 
 def test_good_xml():
     xml = b'<article id="a1">\n<text/>\n</article>'
-    xml_tree = XMLTree()
-    xml_tree.content = xml
-    assert xml == xml_tree.content
+    xml_tree = XMLTree(xml)
+    assert xml_tree.compare(xml)
     assert xml_tree.xml_error is None
     assert xml_tree.tree is not None
 
 
 def test_bad_xml():
     xml = b'<article id="a1">\n<text>\n</article>'
-    xml_tree = XMLTree()
-    xml_tree.content = xml
+    xml_tree = XMLTree(xml)
     assert xml_tree.content is None
     assert xml_tree.xml_error is not None
     assert xml_tree.tree is None
@@ -24,8 +22,7 @@ def test_bad_xml():
 
 def test_compare_equal_xml():
     xml = b'<article id="a2">\n<text/>\n</article>'
-    xml_tree = XMLTree()
-    xml_tree.content = xml
+    xml_tree = XMLTree(xml)
     assert xml_tree.content is not None
     assert xml_tree.compare(
         b'<article id="a2">\n<text/>\n</article>'
@@ -34,9 +31,88 @@ def test_compare_equal_xml():
 
 def test_compare_not_equal_xml():
     xml = b'<article id="a2">\n<text/>\n</article>'
-    xml_tree = XMLTree()
-    xml_tree.content = xml
+    xml_tree = XMLTree(xml)
     assert xml_tree.content is not None
     assert not xml_tree.compare(
         b'<article id="a1">\n<text/>\n</article>'
     )
+
+
+def test_pretty():
+    s_xml = """<article><p>A ljllj <bold>kjjflajfa,</bold> """ \
+        """<italic>djajflaj</italic></p><p>Parágrafo 2</p></article>"""
+    s_expected = """<article>\n  <p>A ljllj <bold>kjjflajfa,</bold> """ \
+        """<italic>djajflaj</italic></p>\n  <p>Parágrafo 2</p>\n</article>\n"""
+
+    xml = s_xml.encode('utf-8')
+    expected = s_expected.encode('utf-8')
+    xml_tree = XMLTree(xml)
+    assert xml_tree.content is not None
+    assert xml_tree.pretty == expected
+
+
+def otimize(s_xml, s_expected):
+    b_xml = s_xml.encode('utf-8')
+    b_expected = s_expected.encode('utf-8')
+    xml_tree = XMLTree(b_xml)
+    assert xml_tree.content is not None
+    assert xml_tree.otimized == b_expected
+
+
+def test_otimized_preserva_tab_linebreak_em_elementos_que_contenham_texto():
+    s_xml = '<article>' \
+        '\n    <body>' \
+        '\n        <p>A ljllj' \
+        '\n            <bold>kjjflajfa,</bold>' \
+        '\n            <italic>djajflaj</italic>' \
+        '\n        </p>' \
+        '\n        a   ' \
+        '\n        <p>Parágrafo 2</p>' \
+        '\n    </body>' \
+        '\n</article>'
+
+    s_expected = '<article>' \
+        '<body>' \
+        '<p>A ljllj' \
+        '\n <bold>kjjflajfa,</bold>' \
+        '\n <italic>djajflaj</italic>' \
+        '\n </p>' \
+        '\n a ' \
+        '\n <p>Parágrafo 2</p>' \
+        '\n </body></article>'
+    otimize(s_xml, s_expected)
+
+
+def test_otimized_preserva_tab_linebreak_em_elementos_que_contenham_texto2():
+    s_xml = '<article> ' \
+        '\nA ljllj\n          \t\t   \n    \t  ' \
+        '\nkjjflajfa,  ' \
+        '\ndjajflaj ' \
+        '\na  ' \
+        '\nParágrafo 2 ' \
+        '\n</article>'
+    s_expected = '<article> ' \
+        '\nA ljllj\n \t\t \n \t ' \
+        '\nkjjflajfa, ' \
+        '\ndjajflaj ' \
+        '\na ' \
+        '\nParágrafo 2 ' \
+        '\n</article>'
+    otimize(s_xml, s_expected)
+
+
+def test_otimized_estilos():
+    s_xml = '<article><b>Bold</b> <i>itálico</i></article>'
+    s_expected = '<article><b>Bold</b><i>itálico</i></article>'
+    otimize(s_xml, s_expected)
+
+
+def test_otimized_elimina_tab_linebreak_em_elementos_que_nao_contem_texto():
+    s_xml = """
+    <article>            \n\t   
+        <p>A ljllj </p>
+        <p>Parágrafo 2</p>\n\n\n\n
+   </article>\n
+    """
+    s_expected = '<article><p>A ljllj </p><p>Parágrafo 2</p></article>'
+    otimize(s_xml, s_expected)