001/*
002 *  Copyright 2018 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.cms.data.type;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022import java.util.Optional;
023import java.util.stream.Stream;
024
025import org.apache.cocoon.xml.AttributesImpl;
026import org.apache.cocoon.xml.XMLUtils;
027import org.apache.commons.lang3.StringUtils;
028import org.apache.commons.lang3.tuple.Triple;
029import org.w3c.dom.Element;
030import org.xml.sax.ContentHandler;
031import org.xml.sax.SAXException;
032
033import org.ametys.cms.data.Geocode;
034import org.ametys.core.model.type.AbstractElementType;
035import org.ametys.core.model.type.ModelItemTypeHelper;
036import org.ametys.runtime.model.ViewItem;
037import org.ametys.runtime.model.compare.DataChangeType;
038import org.ametys.runtime.model.compare.DataChangeTypeDetail;
039import org.ametys.runtime.model.exception.BadItemTypeException;
040import org.ametys.runtime.model.type.DataContext;
041
042/**
043 * Abstract class for geocode type of elements
044 */
045public abstract class AbstractGeocodeElementType extends AbstractElementType<Geocode>
046{
047    /** The separator between the latitude and the longitude for the string representation of a geocode */
048    protected static final String __SEPARATOR = ",";
049    
050    @Override
051    public Geocode convertValue(Object value) throws BadItemTypeException
052    {
053        if (value instanceof String)
054        {
055            String strValue = (String) value;
056            if (StringUtils.isEmpty(strValue))
057            {
058                return null;
059            }
060            
061            if (StringUtils.countMatches(strValue, __SEPARATOR) == 1)
062            {
063                try
064                {
065                    Double latitude = Double.valueOf(StringUtils.substringBeforeLast(strValue, __SEPARATOR));
066                    Double longitude = Double.valueOf(StringUtils.substringAfterLast(strValue, __SEPARATOR));
067                    return new Geocode(latitude, longitude);
068                }
069                catch (NumberFormatException e)
070                {
071                    throw new BadItemTypeException("Unable to cast '" + value + "' to a geocode", e);
072                }
073            }
074        }
075        
076        return super.convertValue(value);
077    }
078    
079    @Override
080    public String toString(Geocode value)
081    {
082        if (value != null)
083        {
084            return String.valueOf(value.getLatitude()) + __SEPARATOR + String.valueOf(value.getLongitude());
085        }
086        else
087        {
088            return null;
089        }
090    }
091    
092    @SuppressWarnings("unchecked")
093    public Object fromJSONForClient(Object json, DataContext context)
094    {
095        if (json == null)
096        {
097            return json;
098        }
099        else if (json instanceof Map)
100        {
101            return _json2Geocode((Map<String, Object>) json);
102        }
103        else if (json instanceof List)
104        {
105            List<Geocode> geocodes = new ArrayList<>();
106            for (Map<String, Object> singleJson : (List<Map<String, Object>>) json)
107            {
108                geocodes.add(_json2Geocode(singleJson));
109            }
110            return geocodes.toArray(new Geocode[geocodes.size()]);
111        }
112        else
113        {
114            throw new IllegalArgumentException("Try to convert the non Geocode JSON object '" + json + "' into a geocode");
115        }
116    }
117    
118    private Geocode _json2Geocode(Map<String, Object> json)
119    {
120        // If the geocode is empty, return null
121        return Optional.ofNullable(json)
122                .filter(geocode -> !geocode.isEmpty()
123                     && geocode.get(Geocode.LATITUDE_IDENTIFIER) != null && geocode.get(Geocode.LATITUDE_IDENTIFIER) instanceof Number
124                     && geocode.get(Geocode.LONGITUDE_IDENTIFIER) != null && geocode.get(Geocode.LONGITUDE_IDENTIFIER) instanceof Number)
125                .map(geocode -> new Geocode(((Number) geocode.get(Geocode.LATITUDE_IDENTIFIER)).doubleValue(), ((Number) geocode.get(Geocode.LONGITUDE_IDENTIFIER)).doubleValue()))
126                .orElse(null);
127    }
128    
129    @Override
130    protected Object _singleValueToJSON(Geocode value, DataContext context)
131    {
132        Map<String, Object> geocodeInfos = new HashMap<>();
133        
134        geocodeInfos.put(Geocode.LATITUDE_IDENTIFIER, value.getLatitude());
135        geocodeInfos.put(Geocode.LONGITUDE_IDENTIFIER, value.getLongitude());
136        
137        return geocodeInfos;
138    }
139    
140    @Override
141    protected Geocode _singleValueFromXML(Element element, Optional<Object> additionalData)
142    {
143        Optional<Double> latitude = _coordinateFromXML(element, Geocode.LATITUDE_IDENTIFIER);
144        Optional<Double> longitude = _coordinateFromXML(element, Geocode.LONGITUDE_IDENTIFIER);
145        
146        if (latitude.isPresent() && longitude.isPresent())
147        {
148            return new Geocode(latitude.get(), longitude.get());
149        }
150        else
151        {
152            return null;
153        }
154    }
155    
156    private Optional<Double> _coordinateFromXML(Element element, String coordinateIdentifier)
157    {
158        String coordinateAsString = element.getAttribute(coordinateIdentifier);
159        return Optional.ofNullable(coordinateAsString)
160                .filter(StringUtils::isNotEmpty)
161                .map(Double::valueOf);
162    }
163
164    @Override
165    protected void _singleValueToSAX(ContentHandler contentHandler, String tagName, Geocode geocode, Optional<ViewItem> viewItem, DataContext context, AttributesImpl attributes) throws SAXException
166    {
167        AttributesImpl localAttributes = new AttributesImpl(attributes);
168        localAttributes.addCDATAAttribute(Geocode.LATITUDE_IDENTIFIER, geocode.getLatitude().toString());
169        localAttributes.addCDATAAttribute(Geocode.LONGITUDE_IDENTIFIER, geocode.getLongitude().toString());
170        XMLUtils.createElement(contentHandler, tagName, localAttributes);
171    }
172    
173    @Override
174    protected Stream<Triple<DataChangeType, DataChangeTypeDetail, String>> _compareMultipleValues(Geocode[] value1, Geocode[] value2)
175    {
176        throw new UnsupportedOperationException("Unable to compare multiple values of type '" + getId() + "'");
177    }
178    
179    @Override
180    protected Stream<Triple<DataChangeType, DataChangeTypeDetail, String>> _compareSingleValues(Geocode value1, Geocode value2)
181    {
182        if (ModelItemTypeHelper.areSingleObjectsBothNotNullAndDifferents(value1, value2))
183        {
184            return Stream.of
185                         (
186                             ModelItemTypeHelper.compareSingleNumbers(value1.getLatitude(), value2.getLatitude(), Geocode.LATITUDE_IDENTIFIER),
187                             ModelItemTypeHelper.compareSingleNumbers(value1.getLongitude(), value2.getLongitude(), Geocode.LONGITUDE_IDENTIFIER)
188                         )
189                         .filter(Optional::isPresent)
190                         .map(Optional::get);
191        }
192        else
193        {
194            return Stream.of(ModelItemTypeHelper.compareSingleObjects(value1, value2, StringUtils.EMPTY))
195                         .filter(Optional::isPresent)
196                         .map(Optional::get);
197        }
198    }
199    
200    public boolean isSimple()
201    {
202        return true;
203    }
204    
205    @Override
206    protected boolean _useJSONForEdition()
207    {
208        return true;
209    }
210}