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