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