1""" Dual ParserNode implementation """
2from certbot_apache._internal import apacheparser
3from certbot_apache._internal import assertions
4from certbot_apache._internal import augeasparser
5
6
7class DualNodeBase:
8    """ Dual parser interface for in development testing. This is used as the
9    base class for dual parser interface classes. This class handles runtime
10    attribute value assertions."""
11
12    def save(self, msg):  # pragma: no cover
13        """ Call save for both parsers """
14        self.primary.save(msg)
15        self.secondary.save(msg)
16
17    def __getattr__(self, aname):
18        """ Attribute value assertion """
19        firstval = getattr(self.primary, aname)
20        secondval = getattr(self.secondary, aname)
21        exclusions = [
22            # Metadata will inherently be different, as ApacheParserNode does
23            # not have Augeas paths and so on.
24            aname == "metadata",
25            callable(firstval)
26        ]
27        if not any(exclusions):
28            assertions.assertEqualSimple(firstval, secondval)
29        return firstval
30
31    def find_ancestors(self, name):
32        """ Traverses the ancestor tree and returns ancestors matching name """
33        return self._find_helper(DualBlockNode, "find_ancestors", name)
34
35    def _find_helper(self, nodeclass, findfunc, search, **kwargs):
36        """A helper for find_* functions. The function specific attributes should
37        be passed as keyword arguments.
38
39        :param interfaces.ParserNode nodeclass: The node class for results.
40        :param str findfunc: Name of the find function to call
41        :param str search: The search term
42        """
43
44        primary_res = getattr(self.primary, findfunc)(search, **kwargs)
45        secondary_res = getattr(self.secondary, findfunc)(search, **kwargs)
46
47        # The order of search results for Augeas implementation cannot be
48        # assured.
49
50        pass_primary = assertions.isPassNodeList(primary_res)
51        pass_secondary = assertions.isPassNodeList(secondary_res)
52        new_nodes = []
53
54        if pass_primary and pass_secondary:
55            # Both unimplemented
56            new_nodes.append(nodeclass(primary=primary_res[0],
57                                       secondary=secondary_res[0]))  # pragma: no cover
58        elif pass_primary:
59            for c in secondary_res:
60                new_nodes.append(nodeclass(primary=primary_res[0],
61                                           secondary=c))
62        elif pass_secondary:
63            for c in primary_res:
64                new_nodes.append(nodeclass(primary=c,
65                                           secondary=secondary_res[0]))
66        else:
67            assert len(primary_res) == len(secondary_res)
68            matches = self._create_matching_list(primary_res, secondary_res)
69            for p, s in matches:
70                new_nodes.append(nodeclass(primary=p, secondary=s))
71
72        return new_nodes
73
74
75class DualCommentNode(DualNodeBase):
76    """ Dual parser implementation of CommentNode interface """
77
78    def __init__(self, **kwargs):
79        """ This initialization implementation allows ordinary initialization
80        of CommentNode objects as well as creating a DualCommentNode object
81        using precreated or fetched CommentNode objects if provided as optional
82        arguments primary and secondary.
83
84        Parameters other than the following are from interfaces.CommentNode:
85
86        :param CommentNode primary: Primary pre-created CommentNode, mainly
87            used when creating new DualParser nodes using add_* methods.
88        :param CommentNode secondary: Secondary pre-created CommentNode
89        """
90
91        kwargs.setdefault("primary", None)
92        kwargs.setdefault("secondary", None)
93        primary = kwargs.pop("primary")
94        secondary = kwargs.pop("secondary")
95
96        if primary or secondary:
97            assert primary and secondary
98            self.primary = primary
99            self.secondary = secondary
100        else:
101            self.primary = augeasparser.AugeasCommentNode(**kwargs)
102            self.secondary = apacheparser.ApacheCommentNode(**kwargs)
103
104        assertions.assertEqual(self.primary, self.secondary)
105
106
107class DualDirectiveNode(DualNodeBase):
108    """ Dual parser implementation of DirectiveNode interface """
109
110    def __init__(self, **kwargs):
111        """ This initialization implementation allows ordinary initialization
112        of DirectiveNode objects as well as creating a DualDirectiveNode object
113        using precreated or fetched DirectiveNode objects if provided as optional
114        arguments primary and secondary.
115
116        Parameters other than the following are from interfaces.DirectiveNode:
117
118        :param DirectiveNode primary: Primary pre-created DirectiveNode, mainly
119            used when creating new DualParser nodes using add_* methods.
120        :param DirectiveNode secondary: Secondary pre-created DirectiveNode
121
122
123        """
124
125        kwargs.setdefault("primary", None)
126        kwargs.setdefault("secondary", None)
127        primary = kwargs.pop("primary")
128        secondary = kwargs.pop("secondary")
129
130        if primary or secondary:
131            assert primary and secondary
132            self.primary = primary
133            self.secondary = secondary
134        else:
135            self.primary = augeasparser.AugeasDirectiveNode(**kwargs)
136            self.secondary = apacheparser.ApacheDirectiveNode(**kwargs)
137
138        assertions.assertEqual(self.primary, self.secondary)
139
140    def set_parameters(self, parameters):
141        """ Sets parameters and asserts that both implementation successfully
142        set the parameter sequence """
143
144        self.primary.set_parameters(parameters)
145        self.secondary.set_parameters(parameters)
146        assertions.assertEqual(self.primary, self.secondary)
147
148
149class DualBlockNode(DualNodeBase):
150    """ Dual parser implementation of BlockNode interface """
151
152    def __init__(self, **kwargs):
153        """ This initialization implementation allows ordinary initialization
154        of BlockNode objects as well as creating a DualBlockNode object
155        using precreated or fetched BlockNode objects if provided as optional
156        arguments primary and secondary.
157
158        Parameters other than the following are from interfaces.BlockNode:
159
160        :param BlockNode primary: Primary pre-created BlockNode, mainly
161            used when creating new DualParser nodes using add_* methods.
162        :param BlockNode secondary: Secondary pre-created BlockNode
163        """
164
165        kwargs.setdefault("primary", None)
166        kwargs.setdefault("secondary", None)
167        primary = kwargs.pop("primary")
168        secondary = kwargs.pop("secondary")
169
170        if primary or secondary:
171            assert primary and secondary
172            self.primary = primary
173            self.secondary = secondary
174        else:
175            self.primary = augeasparser.AugeasBlockNode(**kwargs)
176            self.secondary = apacheparser.ApacheBlockNode(**kwargs)
177
178        assertions.assertEqual(self.primary, self.secondary)
179
180    def add_child_block(self, name, parameters=None, position=None):
181        """ Creates a new child BlockNode, asserts that both implementations
182        did it in a similar way, and returns a newly created DualBlockNode object
183        encapsulating both of the newly created objects """
184
185        primary_new = self.primary.add_child_block(name, parameters, position)
186        secondary_new = self.secondary.add_child_block(name, parameters, position)
187        assertions.assertEqual(primary_new, secondary_new)
188        return DualBlockNode(primary=primary_new, secondary=secondary_new)
189
190    def add_child_directive(self, name, parameters=None, position=None):
191        """ Creates a new child DirectiveNode, asserts that both implementations
192        did it in a similar way, and returns a newly created DualDirectiveNode
193        object encapsulating both of the newly created objects """
194
195        primary_new = self.primary.add_child_directive(name, parameters, position)
196        secondary_new = self.secondary.add_child_directive(name, parameters, position)
197        assertions.assertEqual(primary_new, secondary_new)
198        return DualDirectiveNode(primary=primary_new, secondary=secondary_new)
199
200    def add_child_comment(self, comment="", position=None):
201        """ Creates a new child CommentNode, asserts that both implementations
202        did it in a similar way, and returns a newly created DualCommentNode
203        object encapsulating both of the newly created objects """
204
205        primary_new = self.primary.add_child_comment(comment, position)
206        secondary_new = self.secondary.add_child_comment(comment, position)
207        assertions.assertEqual(primary_new, secondary_new)
208        return DualCommentNode(primary=primary_new, secondary=secondary_new)
209
210    def _create_matching_list(self, primary_list, secondary_list):
211        """ Matches the list of primary_list to a list of secondary_list and
212        returns a list of tuples. This is used to create results for find_
213        methods.
214
215        This helper function exists, because we cannot ensure that the list of
216        search results returned by primary.find_* and secondary.find_* are ordered
217        in a same way. The function pairs the same search results from both
218        implementations to a list of tuples.
219        """
220
221        matched = []
222        for p in primary_list:
223            match = None
224            for s in secondary_list:
225                try:
226                    assertions.assertEqual(p, s)
227                    match = s
228                    break
229                except AssertionError:
230                    continue
231            if match:
232                matched.append((p, match))
233            else:
234                raise AssertionError("Could not find a matching node.")
235        return matched
236
237    def find_blocks(self, name, exclude=True):
238        """
239        Performs a search for BlockNodes using both implementations and does simple
240        checks for results. This is built upon the assumption that unimplemented
241        find_* methods return a list with a single assertion passing object.
242        After the assertion, it creates a list of newly created DualBlockNode
243        instances that encapsulate the pairs of returned BlockNode objects.
244        """
245
246        return self._find_helper(DualBlockNode, "find_blocks", name,
247                                 exclude=exclude)
248
249    def find_directives(self, name, exclude=True):
250        """
251        Performs a search for DirectiveNodes using both implementations and
252        checks the results. This is built upon the assumption that unimplemented
253        find_* methods return a list with a single assertion passing object.
254        After the assertion, it creates a list of newly created DualDirectiveNode
255        instances that encapsulate the pairs of returned DirectiveNode objects.
256        """
257
258        return self._find_helper(DualDirectiveNode, "find_directives", name,
259                                 exclude=exclude)
260
261    def find_comments(self, comment):
262        """
263        Performs a search for CommentNodes using both implementations and
264        checks the results. This is built upon the assumption that unimplemented
265        find_* methods return a list with a single assertion passing object.
266        After the assertion, it creates a list of newly created DualCommentNode
267        instances that encapsulate the pairs of returned CommentNode objects.
268        """
269
270        return self._find_helper(DualCommentNode, "find_comments", comment)
271
272    def delete_child(self, child):
273        """Deletes a child from the ParserNode implementations. The actual
274        ParserNode implementations are used here directly in order to be able
275        to match a child to the list of children."""
276
277        self.primary.delete_child(child.primary)
278        self.secondary.delete_child(child.secondary)
279
280    def unsaved_files(self):
281        """ Fetches the list of unsaved file paths and asserts that the lists
282        match """
283        primary_files = self.primary.unsaved_files()
284        secondary_files = self.secondary.unsaved_files()
285        assertions.assertEqualSimple(primary_files, secondary_files)
286
287        return primary_files
288
289    def parsed_paths(self):
290        """
291        Returns a list of file paths that have currently been parsed into the parser
292        tree. The returned list may include paths with wildcard characters, for
293        example: ['/etc/apache2/conf.d/*.load']
294
295        This is typically called on the root node of the ParserNode tree.
296
297        :returns: list of file paths of files that have been parsed
298        """
299
300        primary_paths = self.primary.parsed_paths()
301        secondary_paths = self.secondary.parsed_paths()
302        assertions.assertEqualPathsList(primary_paths, secondary_paths)
303        return primary_paths
304