001/*
002 *  Copyright 2017 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.extraction.edition;
017
018import java.io.File;
019import java.io.IOException;
020import java.io.OutputStream;
021import java.nio.file.Files;
022import java.nio.file.Path;
023import java.nio.file.Paths;
024import java.nio.file.StandardCopyOption;
025import java.util.Collections;
026import java.util.List;
027import java.util.Map;
028import java.util.Optional;
029import java.util.Properties;
030import java.util.function.BiConsumer;
031import java.util.stream.Collectors;
032import java.util.stream.IntStream;
033import java.util.stream.Stream;
034
035import javax.xml.parsers.DocumentBuilder;
036import javax.xml.parsers.DocumentBuilderFactory;
037import javax.xml.transform.OutputKeys;
038import javax.xml.transform.Transformer;
039import javax.xml.transform.TransformerFactory;
040import javax.xml.transform.dom.DOMSource;
041import javax.xml.transform.sax.SAXTransformerFactory;
042import javax.xml.transform.sax.TransformerHandler;
043import javax.xml.transform.stream.StreamResult;
044
045import org.apache.avalon.framework.service.ServiceException;
046import org.apache.avalon.framework.service.ServiceManager;
047import org.apache.cocoon.xml.AttributesImpl;
048import org.apache.cocoon.xml.XMLUtils;
049import org.apache.commons.lang3.StringUtils;
050import org.apache.excalibur.source.Source;
051import org.apache.excalibur.source.SourceResolver;
052import org.apache.excalibur.source.impl.FileSource;
053import org.apache.xml.serializer.OutputPropertiesFactory;
054import org.w3c.dom.Document;
055import org.w3c.dom.Element;
056import org.w3c.dom.NodeList;
057
058import org.ametys.cms.repository.ContentDAO;
059import org.ametys.cms.workflow.ContentWorkflowHelper;
060import org.ametys.core.ui.Callable;
061import org.ametys.core.ui.StaticClientSideElement;
062import org.ametys.core.user.UserIdentity;
063import org.ametys.core.util.I18nUtils;
064import org.ametys.plugins.core.user.UserHelper;
065import org.ametys.plugins.extraction.ExtractionConstants;
066import org.ametys.plugins.extraction.execution.Extraction.ExtractionProfile;
067import org.ametys.plugins.extraction.execution.Extraction.Visibility;
068import org.ametys.runtime.i18n.I18nizableText;
069
070/**
071 * This client site element manages a button to create an extraction definition file
072 */
073public class EditExtractionClientSideElement extends StaticClientSideElement
074{
075    /** The Avalon role name */
076    public static final String ROLE = EditExtractionClientSideElement.class.getName();
077    
078    private SourceResolver _sourceResolver;
079    private ContentWorkflowHelper _contentWorkflowHelper;
080    private I18nUtils _i18nUtils;
081    private ContentDAO _contentDAO;
082    private UserHelper _userHelper;
083    
084    @Override
085    public void service(ServiceManager serviceManager) throws ServiceException
086    {
087        super.service(serviceManager);
088        _sourceResolver = (SourceResolver) serviceManager.lookup(SourceResolver.ROLE);
089        _contentWorkflowHelper = (ContentWorkflowHelper) serviceManager.lookup(ContentWorkflowHelper.ROLE);
090        _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE);
091        _contentDAO = (ContentDAO) serviceManager.lookup(ContentDAO.ROLE);
092        _userHelper = (UserHelper) serviceManager.lookup(UserHelper.ROLE);
093    }
094
095    /**
096     * Creates an extraction definition file.
097     * @param relativeDefinitionFilePath The path of the extraction definition file to create. This path has to be relative to the base definition directory.
098     * @param language the language used to create the description
099     * @return Map containing success boolean and the created extraction informations, or error codes if one occurs
100     * @throws Exception if an error occurs
101     */
102    @Callable (right = ExtractionConstants.MODIFY_EXTRACTION_RIGHT_ID)
103    public Map<String, Object> createExtraction(String relativeDefinitionFilePath, String language) throws Exception
104    {
105        // Create extraction definitions directory
106        Source definitionsSrc = _sourceResolver.resolveURI(ExtractionConstants.DEFINITIONS_DIR);
107        File definitionsDir = ((FileSource) definitionsSrc).getFile();
108        definitionsDir.mkdirs();
109        
110        String absoluteDefinitionFilePath = ExtractionConstants.DEFINITIONS_DIR + relativeDefinitionFilePath;
111        Source definitionSrc = _sourceResolver.resolveURI(absoluteDefinitionFilePath);
112        File definitionFile = ((FileSource) definitionSrc).getFile();
113
114        if (definitionFile.exists())
115        {
116            getLogger().error("A definition file already exists at path '{}'", relativeDefinitionFilePath);
117            return Map.of(
118                    "success", false,
119                    "error", "already-exists");
120        }
121        
122        try (OutputStream os = Files.newOutputStream(Paths.get(definitionFile.getAbsolutePath())))
123        {
124            // Create the description content
125            String extractionName = _getExtractionNameFromFileName(definitionFile.getName());
126            I18nizableText descriptionTitle = new I18nizableText(ExtractionConstants.PLUGIN_NAME, ExtractionConstants.DESCRIPTION_DEFAULT_TITLE_KEY, Collections.singletonList(extractionName));
127            
128            Map<String, Object> contentInfos = _contentWorkflowHelper.createContent(
129                    ExtractionConstants.DESCRIPTION_CONTENT_WORKFLOW_NAME,
130                    ExtractionConstants.DESCRIPTION_CONTENT_INITIAL_ACTION_ID,
131                    extractionName,
132                    _i18nUtils.translate(descriptionTitle, language),
133                    new String[] {ExtractionConstants.DESCRIPTION_CONTENT_TYPE_ID},
134                    new String[0],
135                    language
136                );
137            String descriptionId = (String) contentInfos.get("contentId");
138            
139            // Create a transformer for saving sax into a file
140            TransformerHandler handler = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
141            
142            StreamResult result = new StreamResult(os);
143            handler.setResult(result);
144
145            // create the format of result
146            Properties format = new Properties();
147            format.put(OutputKeys.METHOD, "xml");
148            format.put(OutputKeys.INDENT, "yes");
149            format.put(OutputKeys.ENCODING, "UTF-8");
150            format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4");
151            handler.getTransformer().setOutputProperties(format);
152            handler.startDocument();
153
154            // sax skeleton
155            XMLUtils.startElement(handler, ExtractionConstants.EXTRACTION_TAG);
156            
157            // Generate SAX events for the extraction's description
158            AttributesImpl attributes = new AttributesImpl();
159            attributes.addCDATAAttribute(ExtractionConstants.DESCRIPTION_IDENTIFIER_ATTRIBUTE_NAME, descriptionId);
160            XMLUtils.createElement(handler, ExtractionConstants.DESCRIPTION_TAG, attributes);
161            
162            // Generate SAX events for the extraction's visibility
163            String visibilityAsString = Visibility.PRIVATE.toString();
164            XMLUtils.createElement(handler, ExtractionConstants.VISIBILITY_TAG, visibilityAsString);
165            
166            // Generate SAX events for the extraction's author
167            UserIdentity author = _currentUserProvider.getUser();
168            _userHelper.saxUserIdentity(author, handler, ExtractionConstants.AUTHOR_TAG);
169            
170            XMLUtils.endElement(handler, ExtractionConstants.EXTRACTION_TAG);
171            
172            handler.endDocument();
173        
174            return Map.of(
175                    "success", true, 
176                    "path", relativeDefinitionFilePath,
177                    "name", definitionFile.getName(),
178                    "descriptionId", descriptionId,
179                    "visibility", visibilityAsString,
180                    "author", _userHelper.user2json(author),
181                    "canRead", true,
182                    "canWrite", true);
183        }
184        catch (Exception e)
185        {
186            getLogger().error("Error when trying to create the extraction definition file '{}'", relativeDefinitionFilePath, e);
187            return Map.of(
188                    "success", false,
189                    "error", "other-error");
190        }
191    }
192    
193    private String _getExtractionNameFromFileName(String fileName)
194    {
195        return fileName.contains(".") ? fileName.substring(0, fileName.lastIndexOf(".")) : fileName;
196    }
197    
198    /**
199     * Adds a description to an extraction.
200     * @param definitionFileName The extraction definition file name
201     * @param descriptionId the identifier of the description 
202     * @return Map containing success boolean and error codes if one occurs
203     * @throws Exception if an error occurs
204     */
205    @Callable (right = ExtractionConstants.MODIFY_EXTRACTION_RIGHT_ID)
206    public Map<String, Object> addDescription(String definitionFileName, String descriptionId) throws Exception
207    {
208        return _modifyDefinitionFile(definitionFileName, descriptionId, this::_insertDescriptionInDocument);
209    }
210
211    private void _insertDescriptionInDocument(Document document, String descriptionId)
212    {
213        Element extractionRoot = document.getDocumentElement();
214        
215        // Delete the current description node if exists
216        _getElements(document.getDocumentElement(), ExtractionConstants.DESCRIPTION_TAG)
217            .forEach(extractionRoot::removeChild);
218
219        // Insert the description in the extraction root node
220        Element description = document.createElement(ExtractionConstants.DESCRIPTION_TAG);
221        description.setAttribute(ExtractionConstants.DESCRIPTION_IDENTIFIER_ATTRIBUTE_NAME, descriptionId);
222        extractionRoot.insertBefore(description, extractionRoot.getFirstChild());
223    }
224    
225    /**
226     * Changes the visibility of an extraction
227     * @param definitionFileName The extraction definition file name
228     * @param visibilityStr The new visibility
229     * @return Map containing success boolean and error codes if one occurs
230     * @throws Exception if an error occurs
231     */
232    @Callable (right = ExtractionConstants.MODIFY_EXTRACTION_RIGHT_ID)
233    public Map<String, Object> changeVisibility(String definitionFileName, String visibilityStr) throws Exception
234    {
235        return _modifyDefinitionFile(definitionFileName, visibilityStr, this::_changeVisibilityInDocument);
236    }
237    
238    private void _changeVisibilityInDocument(Document document, String visibilityStr)
239    {
240        Element extractionRoot = document.getDocumentElement();
241        
242        // Delete the current visibility nodes if exist
243        _getElements(document.getDocumentElement(), ExtractionConstants.VISIBILITY_TAG)
244            .forEach(extractionRoot::removeChild);
245
246        // Insert the visibility in the extraction root node
247        Element visibility = document.createElement(ExtractionConstants.VISIBILITY_TAG);
248        visibility.setTextContent(visibilityStr);
249        extractionRoot.insertBefore(visibility, extractionRoot.getFirstChild());
250    }
251    
252    /**
253     * Assign rights to the given users on the given extraction
254     * @param definitionFileName The extraction definition file name
255     * @param profileId The profile id
256     * @param users The users to grant
257     * @return A result map
258     * @throws Exception if an error occurs
259     */
260    @Callable
261    public Map<String, Object> addGrantedUsers(String definitionFileName, String profileId, List<Map<String, String>> users) throws Exception
262    {
263        ExtractionProfile profile = ExtractionProfile.valueOf(profileId.toUpperCase());
264        Map<String, Object> arguments = Map.of(
265                "profile", profile,
266                "users", users);
267        return _modifyDefinitionFile(definitionFileName, arguments, this::_addGrantedUsers);
268    }
269    
270    @SuppressWarnings("unchecked")
271    private void _addGrantedUsers(Document document, Map<String, Object> arguments)
272    {
273        ExtractionProfile profile = (ExtractionProfile) arguments.get("profile");
274        String rightAccessElementTagName = ExtractionProfile.READ_ACCESS.equals(profile) ? ExtractionConstants.READ_ACCESS_TAG : ExtractionConstants.WRITE_ACCESS_TAG;
275
276        Element extractionRoot = document.getDocumentElement();
277        Element rightAccessElement = _getOrCreateFirstElement(document, extractionRoot, rightAccessElementTagName);
278        Element usersElement = _getOrCreateFirstElement(document, rightAccessElement, ExtractionConstants.USERS_TAG);
279        
280        List<Map<String, String>> users = (List<Map<String, String>>) arguments.get("users");
281        for (Map<String, String> user : users)
282        {
283            Element userElement = document.createElement(ExtractionConstants.USER_TAG);
284            userElement.setAttribute("login", user.get("login"));
285            userElement.setAttribute("population", user.get("populationId"));
286            usersElement.appendChild(userElement);
287        }
288    }
289    
290    /**
291     * Assign rights to the given groups on the given extraction
292     * @param definitionFileName The extraction definition file name
293     * @param profileId The profile id
294     * @param groups The groups to grant
295     * @return A result map
296     * @throws Exception if an error occurs
297     */
298    @Callable
299    public Map<String, Object> addGrantedGroups(String definitionFileName, String profileId, List<Map<String, String>> groups) throws Exception
300    {
301        ExtractionProfile profile = ExtractionProfile.valueOf(profileId.toUpperCase());
302        Map<String, Object> arguments = Map.of(
303                "profile", profile,
304                "groups", groups);
305        return _modifyDefinitionFile(definitionFileName, arguments, this::_addGrantedGroups);
306    }
307    
308    @SuppressWarnings("unchecked")
309    private void _addGrantedGroups(Document document, Map<String, Object> arguments)
310    {
311        ExtractionProfile profile = (ExtractionProfile) arguments.get("profile");
312        String rightAccessElementTagName = ExtractionProfile.READ_ACCESS.equals(profile) ? ExtractionConstants.READ_ACCESS_TAG : ExtractionConstants.WRITE_ACCESS_TAG;
313
314        Element extractionRoot = document.getDocumentElement();
315        Element rightAccessElement = _getOrCreateFirstElement(document, extractionRoot, rightAccessElementTagName);
316        Element groupsElement = _getOrCreateFirstElement(document, rightAccessElement, ExtractionConstants.GROUPS_TAG);
317        
318        List<Map<String, String>> groups = (List<Map<String, String>>) arguments.get("groups");
319        for (Map<String, String> group : groups)
320        {
321            Element groupElement = document.createElement(ExtractionConstants.GROUP_TAG);
322            groupElement.setAttribute("id", group.get("id"));
323            groupElement.setAttribute("groupDirectory", group.get("groupDirectory"));
324            groupsElement.appendChild(groupElement);
325        }
326    }
327    
328    /**
329     * Remove rights to the given users on the given query
330     * @param definitionFileName The extraction definition file name
331     * @param profileId The profile id
332     * @param users The users to remove
333     * @param groups The groups to remove
334     * @return A result map
335     * @throws Exception if an error occurs
336     */
337    @Callable
338    public Map<String, Object> removeAssignment(String definitionFileName, String profileId, List<Map<String, String>> users, List<Map<String, String>> groups) throws Exception
339    {
340        ExtractionProfile profile = ExtractionProfile.valueOf(profileId.toUpperCase());
341        Map<String, Object> arguments = Map.of(
342                "profile", profile,
343                "users", users,
344                "groups", groups);
345        return _modifyDefinitionFile(definitionFileName, arguments, this::_removeAssignement);
346    }
347    
348    @SuppressWarnings("unchecked")
349    private void _removeAssignement(Document document, Map<String, Object> arguments)
350    {
351        ExtractionProfile profile = (ExtractionProfile) arguments.get("profile");
352        String rightAccessElementTagName = ExtractionProfile.READ_ACCESS.equals(profile) ? ExtractionConstants.READ_ACCESS_TAG : ExtractionConstants.WRITE_ACCESS_TAG;
353
354        Element extractionRoot = document.getDocumentElement();
355        Optional<Element> optRightAccessElement = _getFirstElement(extractionRoot, rightAccessElementTagName);
356        
357        if (optRightAccessElement.isPresent())
358        {
359            Element rightAccessElement = optRightAccessElement.get();
360
361            // Users
362            List<Map<String, String>> users = (List<Map<String, String>>) arguments.get("users");
363            Optional<Element> usersElement = _getFirstElement(rightAccessElement, ExtractionConstants.USERS_TAG);
364            if (usersElement.isPresent())
365            {
366                _getElements(document.getDocumentElement(), ExtractionConstants.USER_TAG)
367                    .filter(element -> _containsUser(users, element))
368                    .forEach(extractionRoot::removeChild);
369            }
370            
371            // Groups
372            List<Map<String, String>> groups = (List<Map<String, String>>) arguments.get("groups");
373            Optional<Element> groupsElement = _getFirstElement(rightAccessElement, ExtractionConstants.GROUPS_TAG);
374            if (groupsElement.isPresent())
375            {
376                _getElements(document.getDocumentElement(), ExtractionConstants.GROUP_TAG)
377                    .filter(element -> _containsGroup(groups, element))
378                    .forEach(extractionRoot::removeChild);
379            }
380        }
381    }
382    
383    private boolean _containsUser(List<Map<String, String>> users, Element userElement)
384    {
385        String login = userElement.getAttribute("login");
386        String population = userElement.getAttribute("population");
387        
388        for (Map<String, String> user : users)
389        {
390            if (user.get("login").equals(login) && user.get("populationId").equals(population))
391            {
392                return true;
393            }
394        }
395        
396        // No corresponding group has been found
397        return false; 
398    }
399    
400    private boolean _containsGroup(List<Map<String, String>> groups, Element groupElement)
401    {
402        String groupId = groupElement.getAttribute("id");
403        String groupDirectory = groupElement.getAttribute("groupDirectory");
404        
405        for (Map<String, String> group : groups)
406        {
407            if (group.get("id").equals(groupId) && group.get("groupDirectory").equals(groupDirectory))
408            {
409                return true;
410            }
411        }
412        
413        // No corresponding group has been found
414        return false; 
415    }
416    
417    private <T> Map<String, Object> _modifyDefinitionFile(String definitionFileName, T dataToModify, BiConsumer<Document, T> modifyingConsumer) throws Exception
418    {
419        String definitionFilePath = ExtractionConstants.DEFINITIONS_DIR + definitionFileName;
420        Source definitionSrc = _sourceResolver.resolveURI(definitionFilePath);
421        File definitionFile = ((FileSource) definitionSrc).getFile();
422        
423        if (!definitionFile.exists())
424        {
425            getLogger().error("Error while adding a description to the extraction '{}': this definition file doesn't exist.", definitionFileName);
426            return Map.of(
427                    "success", false,
428                    "error", "unexisting");
429        }
430        
431        String tmpFilePath = ExtractionConstants.DEFINITIONS_DIR + definitionFileName + ".tmp";
432        Source tmpSrc = _sourceResolver.resolveURI(tmpFilePath);
433        File tmpFile = ((FileSource) tmpSrc).getFile();
434        
435        try (OutputStream os = Files.newOutputStream(Paths.get(tmpFile.getAbsolutePath())))
436        {
437            // Parse existing definition file
438            Document document = _parseDefinitionFile(definitionFile);
439            
440            // Apply the modification
441            modifyingConsumer.accept(document, dataToModify);
442
443            // Write the updated definition file
444            DOMSource source = new DOMSource(document);
445            Transformer transformer = TransformerFactory.newInstance().newTransformer();
446            StreamResult result = new StreamResult(os);
447            transformer.transform(source, result);
448            
449            Files.copy(tmpFile.toPath(), definitionFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
450
451            return Map.of("success", true);
452        }
453        catch (Exception e)
454        {
455            getLogger().error("Error when trying to modify the extraction '{}'", definitionFileName, e);
456            return Map.of(
457                    "success", false,
458                    "error", "other-error");
459        }
460        finally
461        {
462            // delete the temporary file to keep the original one
463            _deleteTemporaryFile(definitionFileName, tmpFile.toPath());
464        }
465    }
466    
467    private void _deleteTemporaryFile(String definitionFileName, Path temporaryFilePath)
468    {
469        try
470        {
471            Files.deleteIfExists(temporaryFilePath);
472        }
473        catch (IOException e)
474        {
475            getLogger().error("Error when deleting the temporary file for '{}'", definitionFileName, e);
476        }
477    }
478
479    /**
480     * Renames an extraction definition file.
481     * @param relativeOldFilePath The extraction definition old file path, relative to the base definitions directory
482     * @param newFileName The extraction definition new file name
483     * @return Map containing success boolean and error codes if one occurs
484     * @throws Exception if an error occurs
485     */
486    @Callable (right = ExtractionConstants.MODIFY_EXTRACTION_RIGHT_ID)
487    public Map<String, Object> renameExtraction(String relativeOldFilePath, String newFileName) throws Exception
488    {
489        String asoluteOldFilePath = ExtractionConstants.DEFINITIONS_DIR + relativeOldFilePath;
490        Source oldSrc = _sourceResolver.resolveURI(asoluteOldFilePath);
491        File oldFile = ((FileSource) oldSrc).getFile();
492        
493        if (!oldFile.exists())
494        {
495            getLogger().error("Error while renaming '{}': this definition file doesn't exist.", relativeOldFilePath);
496            return Map.of(
497                    "success", false,
498                    "error", "unexisting");
499        }
500        
501        String relativeParentPath = StringUtils.removeEnd(relativeOldFilePath, oldFile.getName());
502        String relativeNewFilePath = relativeParentPath + newFileName;
503        String absoluteNewFilePath = ExtractionConstants.DEFINITIONS_DIR + relativeNewFilePath;
504        Source newSrc = _sourceResolver.resolveURI(absoluteNewFilePath);
505        File newFile = ((FileSource) newSrc).getFile();
506        
507        if (newFile.exists())
508        {
509            getLogger().error("Error while renaming to '{}': a definition file with this name already exists.", relativeNewFilePath);
510            return Map.of(
511                    "success", false,
512                    "error", "already-exists");
513        }
514
515        try
516        {
517            // Copy old file in the new one
518            Files.copy(oldFile.toPath(), newFile.toPath());
519        }
520        catch (IOException e)
521        {
522            getLogger().error("Error while copying old definition file '{}' in the new one.", relativeOldFilePath, e);
523            return Map.of(
524                    "success", false,
525                    "error", "other-error");
526        }
527        
528        try
529        {
530            Files.deleteIfExists(oldFile.toPath());
531        }
532        catch (IOException e)
533        {
534            getLogger().error("Error while deleting old definition file '{}'", relativeOldFilePath, e);
535            return Map.of(
536                    "success", false,
537                    "error", "other-error");
538        }
539        
540        return Map.of(
541                "success", true, 
542                "path", relativeNewFilePath,
543                "name", newFileName);
544    }
545    
546    /**
547     * Deletes an extraction definition file.
548     * @param definitionFileName The extraction definition file to delete
549     * @return <code>true</code> if extraction deletion succeed, <code>false</code> otherwise
550     * @throws Exception if an error occurs
551     */
552    @Callable (right = ExtractionConstants.MODIFY_EXTRACTION_RIGHT_ID)
553    public boolean deleteExtraction(String definitionFileName) throws Exception
554    {
555        String filePath = ExtractionConstants.DEFINITIONS_DIR + definitionFileName;
556        Source source = _sourceResolver.resolveURI(filePath);
557        File file = ((FileSource) source).getFile();
558        
559        if (!file.exists())
560        {
561            throw new IllegalArgumentException("Error while deleting '" + definitionFileName + "': this definition file doesn't exist.");
562        }
563        
564        try
565        {
566            // Get description identifiers
567            Document document = _parseDefinitionFile(file);
568            List<String> descriptionIds = _getElements(document.getDocumentElement(), ExtractionConstants.DESCRIPTION_TAG)
569                .map(description -> description.getAttribute(ExtractionConstants.DESCRIPTION_IDENTIFIER_ATTRIBUTE_NAME))
570                .collect(Collectors.toList());
571            
572            // Delete descriptions contents
573            _contentDAO.deleteContents(descriptionIds, ExtractionConstants.MODIFY_EXTRACTION_RIGHT_ID);
574            
575            Files.deleteIfExists(file.toPath());
576        }
577        catch (IOException e)
578        {
579            if (getLogger().isErrorEnabled())
580            {
581                getLogger().error("Error while deleting definition file '" + definitionFileName + "'.", e);
582            }
583            throw e;
584        }
585        
586        return true;
587    }
588    
589    private Document _parseDefinitionFile(File definitionFile) throws Exception
590    {
591        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
592        DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
593        return documentBuilder.parse(definitionFile);
594    }
595    
596    private Stream<Element> _getElements(Element extractionRoot, String nodeTagName)
597    {
598        NodeList nodeList = extractionRoot.getElementsByTagName(nodeTagName);
599        return IntStream.range(0, nodeList.getLength())
600                .mapToObj(nodeList::item)
601                .map(Element.class::cast);
602    }
603    
604    private Element _getOrCreateFirstElement(Document document, Element parent, String nodeTagName)
605    {
606        NodeList nodeList = parent.getElementsByTagName(nodeTagName);
607        if (nodeList.getLength() > 0)
608        {
609            return (Element) nodeList.item(0);
610        }
611        else
612        {
613            Element element = document.createElement(nodeTagName);
614            parent.appendChild(element);
615            return element;
616        }
617    }
618    
619    private Optional<Element> _getFirstElement(Element parent, String nodeTagName)
620    {
621        NodeList nodeList = parent.getElementsByTagName(nodeTagName);
622        return nodeList.getLength() > 0 ? Optional.of((Element) nodeList.item(0)) : Optional.empty();
623    }
624}