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.odfsync.apogee.scc;
017
018import java.io.ByteArrayInputStream;
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.math.BigDecimal;
024import java.sql.Clob;
025import java.sql.SQLException;
026import java.util.ArrayList;
027import java.util.Arrays;
028import java.util.Date;
029import java.util.HashMap;
030import java.util.HashSet;
031import java.util.LinkedHashMap;
032import java.util.LinkedHashSet;
033import java.util.LinkedList;
034import java.util.List;
035import java.util.Map;
036import java.util.Set;
037
038import org.apache.avalon.framework.configuration.Configuration;
039import org.apache.avalon.framework.configuration.ConfigurationException;
040import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
041import org.apache.avalon.framework.context.Context;
042import org.apache.avalon.framework.context.ContextException;
043import org.apache.avalon.framework.context.Contextualizable;
044import org.apache.avalon.framework.service.ServiceException;
045import org.apache.avalon.framework.service.ServiceManager;
046import org.apache.cocoon.Constants;
047import org.apache.cocoon.components.ContextHelper;
048import org.apache.cocoon.environment.Request;
049import org.apache.commons.io.IOUtils;
050import org.apache.commons.lang.StringUtils;
051import org.slf4j.Logger;
052
053import org.ametys.cms.content.external.ExternalizableMetadataHelper;
054import org.ametys.cms.content.external.ExternalizableMetadataProvider.ExternalizableMetadataStatus;
055import org.ametys.cms.repository.ModifiableDefaultContent;
056import org.ametys.core.util.JSONUtils;
057import org.ametys.plugins.contentio.ContentImporterHelper;
058import org.ametys.plugins.contentio.synchronize.AbstractSimpleSynchronizableContentsCollection;
059import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollection;
060import org.ametys.plugins.odfsync.apogee.ApogeeDAO;
061import org.ametys.plugins.odfsync.apogee.scc.impl.OrgUnitSynchronizableContentsCollection;
062import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata;
063import org.ametys.plugins.repository.metadata.ModifiableRichText;
064import org.ametys.runtime.config.Config;
065import org.ametys.runtime.i18n.I18nizableText;
066
067import com.google.common.base.CharMatcher;
068import com.google.common.collect.ImmutableList;
069import com.google.common.collect.ImmutableMap;
070
071/**
072 * Abstract class for Apogee synchronization
073 */
074public abstract class AbstractApogeeSynchronizableContentsCollection extends AbstractSimpleSynchronizableContentsCollection implements Contextualizable, ApogeeSynchronizableContentsCollection
075{
076    /**
077     * Request attribute name to store handle contents during import or synchronization.
078     */
079    public static final String HANDLE_CONTENTS = AbstractApogeeSynchronizableContentsCollection.class.getName() + "$handleContents";
080    
081    /** Name of parameter holding the field ID column */
082    protected static final String __PARAM_ID_COLUMN = "idColumn";
083    /** Name of parameter holding the fields mapping */
084    protected static final String __PARAM_MAPPING = "mapping";
085    /** Name of parameter into mapping holding the synchronized property */
086    protected static final String __PARAM_MAPPING_SYNCHRO = "synchro";
087    /** Name of parameter into mapping holding the path of metadata */
088    protected static final String __PARAM_MAPPING_METADATA_REF = "metadata-ref";
089    /** Name of parameter into mapping holding the remote attribute */
090    protected static final String __PARAM_MAPPING_ATTRIBUTE = "attribute";
091    /** Name of parameter holding the criteria */
092    protected static final String __PARAM_CRITERIA = "criteria";
093    /** Name of parameter into criteria holding a criterion */
094    protected static final String __PARAM_CRITERIA_CRITERION = "criterion";
095    /** Name of parameter into criterion holding the id */
096    protected static final String __PARAM_CRITERIA_CRITERION_ID = "id";
097    /** Name of parameter into criterion holding the label */
098    protected static final String __PARAM_CRITERIA_CRITERION_LABEL = "label";
099    /** Name of parameter into criterion holding the type */
100    protected static final String __PARAM_CRITERIA_CRITERION_TYPE = "type";
101    /** Name of paramter holding columns */
102    protected static final String __PARAM_COLUMNS = "columns";
103    /** Name of paramter into columns holding column */
104    protected static final String __PARAM_COLUMNS_COLUMN = "column";
105    
106    /** Default language configured for ODF */
107    protected String _odfLang;
108
109    /** Name of the Apogée column which contains the ID */
110    protected String _idColumn;
111    
112    /** Mapping between metadata and columns */
113    protected Map<String, List<String>> _mapping;
114    
115    /** External fields */
116    protected Set<String> _extFields;
117    
118    /** Synchronized fields */
119    protected Set<String> _syncFields;
120    
121    /** Synchronized fields */
122    protected Set<String> _columns;
123    
124    /** Synchronized fields */
125    protected Set<ApogeeCriterion> _criteria;
126    
127    /** Context */
128    protected Context _context;
129    
130    /** The DAO for remote DB Apogee */
131    protected ApogeeDAO _apogeeDAO;
132    
133    /** The JSON utils */
134    protected JSONUtils _jsonUtils;
135    
136    @Override
137    public void service(ServiceManager manager) throws ServiceException
138    {
139        super.service(manager);
140        _apogeeDAO = (ApogeeDAO) manager.lookup(ApogeeDAO.ROLE);
141        _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
142    }
143
144    @Override
145    public void contextualize(Context context) throws ContextException
146    {
147        _context = context;
148    }
149    
150    @Override
151    protected void configureDataSource(Configuration configuration) throws ConfigurationException
152    {
153        @SuppressWarnings("resource")
154        InputStream is = null;
155        _odfLang = Config.getInstance().getValueAsString("odf.programs.lang");
156        try
157        {
158            org.apache.cocoon.environment.Context ctx = (org.apache.cocoon.environment.Context) _context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
159            File apogeeMapping = new File(ctx.getRealPath("/WEB-INF/param/odf/apogee-mapping.xml"));
160            if (!apogeeMapping.isFile())
161            {
162                is = getClass().getResourceAsStream("/org/ametys/plugins/odfsync/apogee/apogee-mapping.xml");
163            }
164            else
165            {
166                is = new FileInputStream(apogeeMapping);
167            }
168            Configuration cfg = new DefaultConfigurationBuilder().build(is);
169            Configuration child = cfg.getChild(getMappingName());
170            if (child != null)
171            {
172                _criteria = new LinkedHashSet<>();
173                _columns = new LinkedHashSet<>();
174                _idColumn = child.getChild(__PARAM_ID_COLUMN).getValue();
175                _mapping = new HashMap<>();
176                _extFields = new HashSet<>();
177                _syncFields = new HashSet<>();
178                String mappingAsString = child.getChild(__PARAM_MAPPING).getValue();
179                _mapping.put(getIdField(), ImmutableList.of(getIdColumn()));
180                if (StringUtils.isNotEmpty(mappingAsString))
181                {
182                    List<Object> mappingAsList = _jsonUtils.convertJsonToList(mappingAsString);
183                    for (Object object : mappingAsList)
184                    {
185                        @SuppressWarnings("unchecked")
186                        Map<String, Object> field = (Map<String, Object>) object;
187                        
188                        String metadataRef = (String) field.get(__PARAM_MAPPING_METADATA_REF);
189                        
190                        String[] attributes = ((String) field.get(__PARAM_MAPPING_ATTRIBUTE)).split(",");
191                        _mapping.put(metadataRef, Arrays.asList(attributes));
192    
193                        boolean isSynchronized = field.containsKey(__PARAM_MAPPING_SYNCHRO) ? (Boolean) field.get(__PARAM_MAPPING_SYNCHRO) : false;
194                        if (isSynchronized)
195                        {
196                            _syncFields.add(metadataRef);
197                        }
198                        else
199                        {
200                            _extFields.add(metadataRef);
201                        }
202                    }
203                }
204
205                Configuration[] criteria = child.getChild(__PARAM_CRITERIA).getChildren(__PARAM_CRITERIA_CRITERION);
206                for (Configuration criterion : criteria)
207                {
208                    String id = criterion.getChild(__PARAM_CRITERIA_CRITERION_ID).getValue();
209                    I18nizableText label = _getCriterionLabel(criterion.getChild(__PARAM_CRITERIA_CRITERION_LABEL), id);
210                    String type = criterion.getChild(__PARAM_CRITERIA_CRITERION_TYPE).getValue("STRING");
211                    
212                    _criteria.add(new ApogeeCriterion(id, label, type));
213                }
214
215                Configuration[] columns = child.getChild(__PARAM_COLUMNS).getChildren(__PARAM_COLUMNS_COLUMN);
216                for (Configuration column : columns)
217                {
218                    _columns.add(column.getValue());
219                }
220            }
221        }
222        catch (Exception e)
223        {
224            throw new ConfigurationException("Error while parsing apogee-mapping.xml", e);
225        }
226        finally
227        {
228            IOUtils.closeQuietly(is);
229        }
230    }
231    
232    private I18nizableText _getCriterionLabel(Configuration configuration, String defaultValue)
233    {
234        if (configuration.getAttributeAsBoolean("i18n", false))
235        {
236            return new I18nizableText("plugin.odf-sync", configuration.getValue(defaultValue));
237        }
238        else
239        {
240            return new I18nizableText(configuration.getValue(defaultValue));
241        }
242    }
243    
244    @Override
245    public List<ModifiableDefaultContent> populate(Logger logger)
246    {
247        boolean isRequestAttributeOwner = false;
248        
249        Request request = ContextHelper.getRequest(_context);
250        if (request.getAttribute(HANDLE_CONTENTS) == null)
251        {
252            request.setAttribute(HANDLE_CONTENTS, new HashSet<String>());
253            isRequestAttributeOwner = true;
254        }
255        
256        List<ModifiableDefaultContent> populatedContents = super.populate(logger);
257        
258        if (isRequestAttributeOwner)
259        {
260            request.removeAttribute(HANDLE_CONTENTS);
261        }
262
263        return populatedContents;
264    }
265    
266    @Override
267    protected Map<String, Map<String, Object>> internalSearch(Map<String, Object> parameters, int offset, int limit, List<Object> sort, Logger logger)
268    {
269        Map<String, Object> searchParams = new HashMap<>(parameters);
270        if (offset > 0)
271        {
272            searchParams.put("__offset", offset);
273        }
274        if (limit < Integer.MAX_VALUE)
275        {
276            searchParams.put("__limit", offset + limit);
277        }
278        searchParams.put("__order", getSort(sort));
279
280        // We don't use session.selectMap which reorder data
281        List<Map<String, Object>> requestValues = _search(searchParams, logger);
282
283        String idColumn = getIdColumn();
284        Map<String, Map<String, Object>> results = new LinkedHashMap<>();
285        for (Map<String, Object> contentValues : requestValues)
286        {
287            results.put(contentValues.get(idColumn).toString(), contentValues);
288        }
289        
290        for (Map<String, Object> result : results.values())
291        {
292            result.put(SCC_UNIQUE_ID, result.get(getIdColumn()));
293        }
294        
295        return results;
296    }
297    
298    @Override
299    protected Map<String, Map<String, List<Object>>> getRemoteValues(Map<String, Object> parameters, Logger logger)
300    {
301        Map<String, Map<String, List<Object>>> remoteValues = new HashMap<>();
302        
303        Map<String, Map<String, Object>> results = internalSearch(parameters, 0, Integer.MAX_VALUE, null, logger);
304        
305        if (results != null)
306        {
307            remoteValues = _sccHelper.organizeRemoteValuesByMetadata(results, _mapping);
308        }
309        
310        return remoteValues;
311    }
312    
313    @Override
314    protected List<ModifiableDefaultContent> _importOrSynchronizeContent(String idValue, Map<String, List<Object>> remoteValues, boolean forceImport, Logger logger)
315    {
316        return _importOrSynchronizeContent(idValue, _odfLang, remoteValues, forceImport, logger);
317    }
318
319    @Override
320    public List<ModifiableDefaultContent> importContent(String idValue, Map<String, Object> importParams, Logger logger) throws Exception
321    {
322        boolean isRequestAttributeOwner = false;
323        
324        Request request = ContextHelper.getRequest(_context);
325        if (request.getAttribute(HANDLE_CONTENTS) == null)
326        {
327            request.setAttribute(HANDLE_CONTENTS, new HashSet<String>());
328            isRequestAttributeOwner = true;
329        }
330
331        List<ModifiableDefaultContent> createdContents = new ArrayList<>();
332        
333        Map<String, Object> parameters = putIdParameter(idValue);
334        Map<String, Map<String, List<Object>>> results = getTransformedRemoteValues(parameters, logger);
335        if (!results.isEmpty())
336        {
337            try
338            {
339                createdContents.add(_importContent(idValue, importParams, _odfLang, results.get(idValue), logger));
340            }
341            catch (Exception e)
342            {
343                _nbError++;
344                logger.error("An error occurred while importing or synchronizing content", e);
345            }
346        }
347        
348        if (isRequestAttributeOwner)
349        {
350            request.removeAttribute(HANDLE_CONTENTS);
351        }
352
353        return createdContents;
354    }
355    
356    @Override
357    public void synchronizeContent(ModifiableDefaultContent content, Logger logger) throws Exception
358    {
359        boolean isRequestAttributeOwner = false;
360        
361        Request request = ContextHelper.getRequest(_context);
362        if (request.getAttribute(HANDLE_CONTENTS) == null)
363        {
364            request.setAttribute(HANDLE_CONTENTS, new HashSet<String>());
365            isRequestAttributeOwner = true;
366        }
367        
368        super.synchronizeContent(content, logger);
369        
370        if (isRequestAttributeOwner)
371        {
372            request.removeAttribute(HANDLE_CONTENTS);
373        }
374    }
375    
376    @Override
377    protected Map<String, Object> putIdParameter(String idValue)
378    {
379        Map<String, Object> parameters = new HashMap<>();
380        parameters.put(getIdField(), idValue);
381        return parameters;
382    }
383    
384    /**
385     * Search the contents with the search parameters. Use id parameter to search an unique content.
386     * @param searchParams Search parameters
387     * @param logger The logger
388     * @return A Map of mapped metadatas extract from Apogée database ordered by content unique Apogée ID
389     */
390    protected abstract List<Map<String, Object>> _search(Map<String, Object> searchParams, Logger logger);
391    
392    /**
393     * Convert the {@link BigDecimal} values retrieved from database into long values
394     * @param searchResults The initial search results from database
395     * @return The converted search results
396     */
397    protected List<Map<String, Object>> _convertBigDecimal(List<Map<String, Object>> searchResults)
398    {
399        List<Map<String, Object>> convertedSearchResults = new ArrayList<>();
400        
401        for (Map<String, Object> searchResult : searchResults)
402        {
403            for (String key : searchResult.keySet())
404            {
405                if (searchResult.get(key) instanceof BigDecimal)
406                {
407                    searchResult.put(key, ((BigDecimal) searchResult.get(key)).longValue()); 
408                }
409            }
410            
411            convertedSearchResults.add(searchResult);
412        }
413        
414        return convertedSearchResults;
415    }
416    
417    /**
418     * Transform CLOB value to String value.
419     * @param value The input value
420     * @param idValue The identifier of the program
421     * @param logger The logger
422     * @return the same value, with CLOB transformed to String.
423     */
424    protected Object _transformClobToString(Object value, String idValue, Logger logger)
425    {
426        if (value instanceof Clob)
427        {
428            Clob clob = (Clob) value;
429            try
430            {
431                String strValue = IOUtils.toString(clob.getCharacterStream());
432                return CharMatcher.javaIsoControl().and(CharMatcher.anyOf("\r\n\t").negate()).removeFrom(strValue);
433            }
434            catch (SQLException | IOException e)
435            {
436                logger.error("Unable to get education add elements from the program '{}'.", idValue, e);
437                return null;
438            }
439            finally
440            {
441                try
442                {
443                    clob.free();
444                }
445                catch (SQLException e)
446                {
447                    // Ignore the exception.
448                }
449            }
450        }
451        
452        return value;
453    }
454    
455    /**
456     * Get the name of the mapping.
457     * @return the mapping name
458     */
459    protected abstract String getMappingName();
460    
461    /**
462     * Get the identifier column (can be a concatened column).
463     * @return the column id
464     */
465    protected String getIdColumn()
466    {
467        return _idColumn;
468    }
469    
470    @Override
471    public String getIdField()
472    {
473        return "apogeeSyncCode";
474    }
475
476    @Override
477    public Set<String> getLocalAndExternalFields(Map<String, Object> additionalParameters)
478    {
479        return _syncFields;
480    }
481
482    @Override
483    public Set<String> getExternalOnlyFields(Map<String, Object> additionalParameters)
484    {
485        return _extFields;
486    }
487    
488    @Override
489    protected boolean _fillContent(Map<String, List<Object>> remoteValues, ModifiableDefaultContent content, boolean create, Logger logger)
490    {
491        Map<String, Object> params = ImmutableMap.of("contentType", getContentType());
492        ModifiableCompositeMetadata holder = content.getMetadataHolder();
493        
494        for (String metadataName : getRichTextFields())
495        {
496            if (remoteValues.containsKey(metadataName))
497            {
498                List<String> lines = new LinkedList<>();
499                for (Object remoteText : remoteValues.get(metadataName))
500                {
501                    lines.add(remoteText.toString());
502                }
503                
504                String docbook = ContentImporterHelper.textToDocbook(lines.toArray(new String[lines.size()]));
505                boolean synchronize = getLocalAndExternalFields(params).contains(metadataName);
506                
507                try (ByteArrayInputStream is =  new ByteArrayInputStream(docbook.getBytes("UTF-8")))
508                {
509                    ModifiableRichText richText = ExternalizableMetadataHelper.getRichText(holder, metadataName, synchronize ? ExternalizableMetadataStatus.EXTERNAL : ExternalizableMetadataStatus.LOCAL, true);
510                    richText.setInputStream(is);
511                    richText.setMimeType("text/xml");
512                    richText.setLastModified(new Date()); 
513                }
514                catch (IOException e)
515                {
516                    logger.error("An error occured while parsing the rich text '{}' of the content '{}'", metadataName, content.getTitle(), e);
517                }
518                
519                remoteValues.remove(metadataName);
520            }
521        }
522        
523        boolean hasChanges = super._fillContent(remoteValues, content, create, logger);
524        hasChanges = _handleAdditionalMetadata(holder, create) || hasChanges;
525        
526        if (!holder.hasMetadata(getIdField()))
527        {
528            holder.setMetadata(getIdField(), remoteValues.get(getIdField()).get(0).toString());
529            hasChanges |= create;
530        }
531        
532        return hasChanges;
533    }
534
535    /**
536     * Method to add additional metadata on import or synchronize.
537     * @param holder The holder of the content to update
538     * @param create If we are on creation mode
539     * @return <code>true</code> if changes has been made
540     */
541    protected boolean _handleAdditionalMetadata(ModifiableCompositeMetadata holder, boolean create)
542    {
543        // Do nothing
544        return false;
545    }
546
547    /**
548     * Get the list of rich text fields of the imported content.
549     * @return The list of the rich text fields metadata name
550     */
551    protected Set<String> getRichTextFields()
552    {
553        return new HashSet<>();
554    }
555    
556    @Override
557    protected void configureSearchModel()
558    {
559        for (ApogeeCriterion criterion : _criteria)
560        {
561            _searchModelConfiguration.addCriterion(criterion.getId(), criterion.getLabel(), criterion.getType());
562        }
563        for (String columnName : _columns)
564        {
565            _searchModelConfiguration.addColumn(columnName);
566        }
567    }
568    
569    @Override
570    protected boolean additionalImportOperations(ModifiableDefaultContent content, Map<String, List<Object>> remoteValues, Map<String, Object> importParams, Logger logger)
571    {
572        boolean hasChanges = super.additionalImportOperations(content, remoteValues, importParams, logger);
573
574        Object parentId = importParams != null && importParams.containsKey("parentId") ? importParams.get("parentId") : null;
575        ModifiableDefaultContent parentContent = parentId != null ? _resolver.resolveById(parentId.toString()) : null;
576        
577        hasChanges = handleParent(content, parentContent, logger) || hasChanges;
578        hasChanges = handleChildren(content, logger) || hasChanges;
579        hasChanges = setAdditionalMetadata(content, remoteValues, logger) || hasChanges;
580        
581        return hasChanges;
582    }
583    
584    @Override
585    protected boolean additionalSynchronizeOperations(ModifiableDefaultContent content, Map<String, List<Object>> remoteValues, Logger logger)
586    {
587        boolean hasChanges = super.additionalSynchronizeOperations(content, remoteValues, logger);
588        hasChanges = additionalImportOperations(content, remoteValues, null, logger) || hasChanges;
589        return hasChanges;
590    }
591    
592    /**
593     * Set the parent metadata and invert relation.
594     * @param currentContent Current content
595     * @param parentContent Parent content to set
596     * @param logger The logger
597     * @return <code>true</code> if there are changes
598     */
599    protected boolean handleParent(ModifiableDefaultContent currentContent, ModifiableDefaultContent parentContent, Logger logger)
600    {
601        // Nothing to do by default
602        return false;
603    }
604    
605    /**
606     * Set the children metadata and invert relation, import and synchronize the children too.
607     * @param content Current content
608     * @param logger The logger
609     * @return <code>true</code> if there are changes
610     */
611    protected boolean handleChildren(ModifiableDefaultContent content, Logger logger)
612    {
613        // Nothing to do by default
614        return false;
615    }
616
617    /**
618     * Set the additional metadata.
619     * @param content Current content
620     * @param remoteValues Values of the content
621     * @param logger The logger
622     * @return <code>true</code> if there are changes
623     */
624    protected boolean setAdditionalMetadata(ModifiableDefaultContent content, Map<String, List<Object>> remoteValues, Logger logger)
625    {
626        // Nothing to do by default
627        return false;
628    }
629    
630    @SuppressWarnings("unchecked")
631    private String getSort(List<Object> sortList)
632    {
633        if (sortList != null)
634        {
635            StringBuilder sort = new StringBuilder();
636            
637            for (Object sortValueObj : sortList)
638            {
639                Map<String, Object> sortValue = (Map<String, Object>) sortValueObj;
640                
641                sort.append(sortValue.get("property"));
642                if (sortValue.containsKey("direction"))
643                {
644                    sort.append(" ");
645                    sort.append(sortValue.get("direction"));
646                    sort.append(",");
647                }
648                else
649                {
650                    sort.append(" ASC,");
651                }
652            }
653            
654            sort.deleteCharAt(sort.length() - 1);
655            
656            return sort.toString();
657        }
658        
659        return null;
660    }
661    
662    @Override
663    public int getTotalCount(Map<String, Object> parameters, Logger logger)
664    {
665        // Remove empty parameters
666        Map<String, Object> searchParams = new HashMap<>();
667        for (String parameterName : parameters.keySet())
668        {
669            Object parameterValue = parameters.get(parameterName);
670            if (parameterValue != null && !parameterValue.toString().isEmpty())
671            {
672                searchParams.put(parameterName, parameterValue);
673            }
674        }
675        
676        searchParams.put("__count", true);
677        
678        List<Map<String, Object>> results = _search(searchParams, logger);
679        if (results != null && !results.isEmpty())
680        {
681            return Integer.valueOf(results.get(0).get("COUNT(*)").toString()).intValue();
682        }
683        
684        return 0;
685    }
686    
687    @Override
688    protected ModifiableDefaultContent _importContent(String idValue, Map<String, Object> importParams, String lang, Map<String, List<Object>> remoteValues, Logger logger) throws Exception
689    {
690        ModifiableDefaultContent content = super._importContent(idValue, importParams, lang, _transformOrgUnitMetadata(remoteValues, logger), logger);
691        if (content != null)
692        {
693            addToHandleContents(content.getId());
694        }
695        return content;
696    }
697    
698    @Override
699    protected ModifiableDefaultContent _synchronizeContent(ModifiableDefaultContent content, Map<String, List<Object>> remoteValues, Logger logger) throws Exception
700    {
701        if (!addToHandleContents(content.getId()))
702        {
703            return content;
704        }
705        return super._synchronizeContent(content, _transformOrgUnitMetadata(remoteValues, logger), logger);
706    }
707    
708    /**
709     * Import and synchronize children of the given content, then edit the structure of the content and its children.
710     * @param content Parent content
711     * @param sccModelId SCC model ID
712     * @param metadataName Metadata name to set
713     * @param invertMetadataName Metadata name of the invert relation
714     * @param logger The logger
715     * @return <code>true</code> if there are changes
716     */
717    protected boolean importOrSynchronizeChildren(ModifiableDefaultContent content, String sccModelId, String metadataName, String invertMetadataName, Logger logger)
718    {
719        boolean hasChanges = false;
720        
721        ModifiableCompositeMetadata cm = content.getMetadataHolder();
722        
723        if (removalSync() && cm.hasMetadata(metadataName))
724        {
725            String[] children = cm.getStringArray(metadataName);
726            hasChanges = ExternalizableMetadataHelper.removeMetadataIfExists(cm, metadataName);
727            
728            for (String child : children)
729            {
730                ModifiableDefaultContent childContent = _resolver.resolveById(child);
731                if (child != null)
732                {
733                    hasChanges = _updateRelation(childContent.getMetadataHolder(), invertMetadataName, content, true) || hasChanges;
734                }
735            }
736        }
737        
738        // Get the SCC for children
739        SynchronizableContentsCollection scc = _sccHelper.getSCCFromModelId(sccModelId);
740
741        // Search for children
742        if (scc != null && scc instanceof ApogeeSynchronizableContentsCollection)
743        {
744            String syncCode = cm.getString(getIdField());
745            List<ModifiableDefaultContent> children = ((ApogeeSynchronizableContentsCollection) scc).importOrSynchronizeContents(ImmutableMap.of("parentCode", syncCode), logger);
746            
747            hasChanges = ExternalizableMetadataHelper.setMetadata(cm, metadataName, children.toArray(new ModifiableDefaultContent[children.size()])) || hasChanges;
748            for (ModifiableDefaultContent child : children)
749            {
750                hasChanges = _updateRelation(child.getMetadataHolder(), invertMetadataName, content) || hasChanges;
751            }
752        }
753        
754        return hasChanges;
755    }
756
757    @Override
758    public List<ModifiableDefaultContent> importOrSynchronizeContents(Map<String, Object> searchParams, Logger logger)
759    {
760        return _importOrSynchronizeContents(searchParams, true, logger);
761    }
762    
763    /**
764     * Add the content ID to the handle contents list.
765     * @param contentId Content ID
766     * @return <code>true</code> if the content ID have been added, <code>false</code> is returned if the content ID already exists in the handle contents list.
767     */
768    protected boolean addToHandleContents(String contentId)
769    {
770        Request request = ContextHelper.getRequest(_context);
771        @SuppressWarnings("unchecked")
772        Set<String> handleContents = (Set<String>) request.getAttribute(HANDLE_CONTENTS);
773        boolean added = handleContents.add(contentId);
774        request.setAttribute(HANDLE_CONTENTS, handleContents);
775        return added;
776    }
777
778    @SuppressWarnings("unchecked")
779    private Map<String, List<Object>> _transformOrgUnitMetadata(Map<String, List<Object>> remoteValues, Logger logger)
780    {
781        // Transform orgUnit values and import content if necessary (useful for Course and SubProgram)
782        SynchronizableContentsCollection scc = _sccHelper.getSCCFromModelId(OrgUnitSynchronizableContentsCollection.MODEL_ID);
783        
784        List<Object> orgUnitCodes = remoteValues.get("orgUnit");
785        if (orgUnitCodes != null && !orgUnitCodes.isEmpty())
786        {
787            List<?> orgUnitContents = null;
788            String orgUnitCode = orgUnitCodes.get(0).toString();
789            
790            if (scc != null)
791            {
792                try
793                {
794                    ModifiableDefaultContent orgUnitContent = scc.getContent(_odfLang, orgUnitCode);
795                    if (orgUnitContent == null)
796                    {
797                        orgUnitContents = scc.importContent(orgUnitCode, null, logger);
798                    }
799                    else
800                    {
801                        orgUnitContents = ImmutableList.of(orgUnitContent);
802                    }
803                }
804                catch (Exception e)
805                {
806                    logger.error("An error occured during the import of the OrgUnit identified by the synchronization code '{}'", orgUnitCode, e);
807                }
808            }
809            
810            if (orgUnitContents == null)
811            {
812                // Impossible link to orgUnit
813                remoteValues.remove("orgUnit");
814                logger.warn("Impossible to import the OrgUnit with the synchronization code '{}', check if you set the OrgUnit SCC with the following model ID: '{}'", orgUnitCode, OrgUnitSynchronizableContentsCollection.MODEL_ID);
815            }
816            else
817            {
818                remoteValues.put("orgUnit", (List<Object>) orgUnitContents);
819            }
820        }
821        
822        return remoteValues;
823    }
824
825    @Override
826    public boolean handleRightAssignmentContext()
827    {
828        // Rights on ODF contents are handled by ODFRightAssignmentContext
829        return false;
830    }
831}