/*
* Data Hub Service (DHuS) - For Space data distribution.
* Copyright (C) 2013,2014,2015,2016 GAEL Systems
*
* This file is part of DHuS software sources.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package fr.gael.dhus.olingo.v1.entity;
import fr.gael.dhus.database.object.SynchronizerConf;
import fr.gael.dhus.olingo.v1.ExpectedException;
import fr.gael.dhus.olingo.v1.ExpectedException.IncompleteDocException;
import fr.gael.dhus.olingo.v1.ExpectedException.InvalidTargetException;
import fr.gael.dhus.olingo.v1.ExpectedException.InvalidValueException;
import fr.gael.dhus.olingo.v1.ExpectedException.NoTargetException;
import fr.gael.dhus.olingo.v1.Navigator;
import fr.gael.dhus.olingo.v1.Model;
import fr.gael.dhus.olingo.v1.entityset.SynchronizerEntitySet;
import fr.gael.dhus.service.CollectionService;
import fr.gael.dhus.service.ISynchronizerService;
import fr.gael.dhus.service.exception.InvokeSynchronizerException;
import fr.gael.dhus.spring.context.ApplicationContextProvider;
import fr.gael.dhus.sync.SynchronizerStatus;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.StringTokenizer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.olingo.odata2.api.edm.Edm;
import org.apache.olingo.odata2.api.edm.EdmEntitySet;
import org.apache.olingo.odata2.api.ep.entry.ODataEntry;
import org.apache.olingo.odata2.api.exception.ODataException;
import org.apache.olingo.odata2.api.rt.RuntimeDelegate;
import org.apache.olingo.odata2.api.uri.KeyPredicate;
import org.apache.olingo.odata2.api.uri.NavigationSegment;
import org.apache.olingo.odata2.api.uri.PathSegment;
import org.apache.olingo.odata2.api.uri.UriInfo;
import org.apache.olingo.odata2.api.uri.UriParser;
/**
* Synchronizer OData Entity.
*/
public final class Synchronizer extends AbstractEntity
{
/** Log. */
private static final Logger LOGGER = LogManager.getLogger(Synchronizer.class);
/** Database Object. */
private final SynchronizerConf syncConf;
/** Synchronizer Service, to create new {@link SynchronizerConf}. */
private static final ISynchronizerService SYNCHRONIZER_SERVICE =
ApplicationContextProvider.getBean (ISynchronizerService.class);
/** Collection Service, for TargetCollection. */
private static final CollectionService COLLECTION_SERVICE =
ApplicationContextProvider.getBean (CollectionService.class);
/**
* Creates a new Synchronizer from its ID.
*
* @param sync_id synchronizer's ID.
* @throws NullPointerException if no synchronizer has the given ID.
*/
public Synchronizer (long sync_id)
{
this (SYNCHRONIZER_SERVICE.getSynchronizerConfById (sync_id));
}
/**
* Creates a new Synchronizer from a database object.
*
* @param sync_conf database object.
*/
public Synchronizer (SynchronizerConf sync_conf)
{
Objects.requireNonNull (sync_conf);
this.syncConf = sync_conf;
}
/**
* Creates an new Synchronizer from the given ODataEntry.
*
* @param odata_entry created by a POST request on the OData interface.
* @throws ODataException if the given entry is malformed.
*/
public Synchronizer (ODataEntry odata_entry) throws ODataException
{
Map<String, Object> props = odata_entry.getProperties ();
String label = (String) props.get(SynchronizerEntitySet.LABEL);
String schedule = (String) props.get(SynchronizerEntitySet.SCHEDULE);
String request = (String) props.get(SynchronizerEntitySet.REQUEST);
String service_url = (String) props.get(SynchronizerEntitySet.SERVICE_URL);
if (schedule == null || schedule.isEmpty () || service_url == null ||
service_url.isEmpty ())
{
throw new IncompleteDocException();
}
if (request != null && !request.equals ("start") &&
!request.equals ("stop"))
{
throw new InvalidValueException(SynchronizerEntitySet.REQUEST, request);
}
try
{
this.syncConf =
SYNCHRONIZER_SERVICE.createSynchronizer (label,
"ODataProductSynchronizer", schedule);
updateFromEntry (odata_entry);
}
catch (ParseException e)
{
throw new ExpectedException(e.getMessage());
}
}
/**
* Returns the TargetCollection, or null if there is none.
*
* @return the TargetCollection.
*/
public Collection getTargetCollection () throws ODataException
{
String target = this.syncConf.getConfig ("target_collection");
if (target == null)
{
return null;
}
fr.gael.dhus.database.object.Collection c =
COLLECTION_SERVICE.getCollection (target);
if (c == null)
{
throw new ODataException (
"This synchronizer references a deleted collection");
}
return new Collection (c);
}
/**
* Deletes a synchronizer having the given id.
*
* @param sync_id ID of the synchronizer to delete.
*/
public static void delete (long sync_id)
{
SYNCHRONIZER_SERVICE.removeSynchronizer (sync_id);
}
@Override
public void updateFromEntry (ODataEntry odata_entry) throws ODataException
{
Map<String, Object> props = odata_entry.getProperties ();
String schedule = (String) props.remove(SynchronizerEntitySet.SCHEDULE);
String request = (String) props.remove(SynchronizerEntitySet.REQUEST);
String service_url = (String) props.remove(SynchronizerEntitySet.SERVICE_URL);
Integer page_size = (Integer) props.remove(SynchronizerEntitySet.PAGE_SIZE);
Boolean copy_product = (Boolean) props.remove(SynchronizerEntitySet.COPY_PRODUCT);
// Nullable fields
boolean has_label = props.containsKey(SynchronizerEntitySet.LABEL);
boolean has_login = props.containsKey(SynchronizerEntitySet.SERVICE_LOGIN);
boolean has_password = props.containsKey(SynchronizerEntitySet.SERVICE_PASSWORD);
boolean has_incoming = props.containsKey(SynchronizerEntitySet.REMOTE_INCOMING);
boolean has_filter = props.containsKey(SynchronizerEntitySet.FILTER_PARAM);
boolean has_collec = props.containsKey(SynchronizerEntitySet.SOURCE_COLLECTION);
boolean has_last_date = props.containsKey(SynchronizerEntitySet.LAST_INGESTION_DATE);
boolean has_target_col = editsTargetCollection(odata_entry);
String label = (String) props.remove(SynchronizerEntitySet.LABEL);
String service_login = (String) props.remove(SynchronizerEntitySet.SERVICE_LOGIN);
String service_password = (String) props.remove(SynchronizerEntitySet.SERVICE_PASSWORD);
String remote_incoming = (String) props.remove(SynchronizerEntitySet.REMOTE_INCOMING);
String filter_param = (String) props.remove(SynchronizerEntitySet.FILTER_PARAM);
String source_collection = (String) props.remove(SynchronizerEntitySet.SOURCE_COLLECTION);
GregorianCalendar last_ingestion_date =
(GregorianCalendar) props.remove(SynchronizerEntitySet.LAST_INGESTION_DATE);
// Navigation
Collection target_collection = getTargetCollection(odata_entry);
for (String pname : props.keySet ())
{
LOGGER.debug ("Unknown or ReadOnly property: " + pname);
}
if (request != null)
{
if (request.equals ("start"))
{
this.syncConf.setActive (true);
}
else
if (request.equals ("stop"))
{
this.syncConf.setActive (false);
}
else
{
throw new InvalidValueException(SynchronizerEntitySet.SCHEDULE, request);
}
}
if (schedule != null && !schedule.isEmpty ())
{
try
{
this.syncConf.setCronExpression (schedule);
}
catch (ParseException ex)
{
throw new ExpectedException(ex.getMessage());
}
}
if (has_label)
{
this.syncConf.setLabel (label);
}
if (service_url != null && !service_url.isEmpty ())
{
this.syncConf.setConfig ("service_uri", service_url);
}
if (has_login)
{
updateNullableProperty("service_username", service_login);
}
if (has_password)
{
updateNullableProperty("service_password", service_password);
}
if (page_size != null)
{
this.syncConf.setConfig ("page_size", page_size.toString ());
}
if (has_incoming)
{
updateNullableProperty("remote_incoming_path", remote_incoming);
}
if (has_last_date)
{
String date = last_ingestion_date != null ?
String.valueOf(last_ingestion_date.getTime().getTime())
: null;
updateNullableProperty("last_created", date);
}
if (copy_product != null)
{
this.syncConf.setConfig ("copy_product", copy_product.toString ());
}
if (has_target_col)
{
if (target_collection == null)
{
this.syncConf.removeConfig("target_collection");
}
else
{
this.syncConf.setConfig("target_collection", target_collection.getUUID());
}
}
if (has_filter)
{
updateNullableProperty("filter_param", filter_param);
}
if (has_collec)
{
updateNullableProperty("source_collection", source_collection);
}
try
{
SYNCHRONIZER_SERVICE.saveSynchronizerConf (this.syncConf);
}
catch (InvokeSynchronizerException e)
{
throw new ODataException (e);
}
}
@Override
public Map<String, Object> toEntityResponse (String root_url)
{
SynchronizerStatus ss = SYNCHRONIZER_SERVICE.getStatus (this.syncConf);
Map<String, Object> res = new HashMap<> ();
res.put(SynchronizerEntitySet.ID, this.syncConf.getId());
res.put(SynchronizerEntitySet.LABEL, this.syncConf.getLabel());
res.put(SynchronizerEntitySet.SCHEDULE, this.syncConf.getCronExpression());
res.put(SynchronizerEntitySet.REQUEST, this.syncConf.getActive() ? "start" : "stop");
res.put(SynchronizerEntitySet.STATUS, ss.status.toString());
res.put(SynchronizerEntitySet.STATUS_DATE, ss.since);
res.put(SynchronizerEntitySet.STATUS_MESSAGE, ss.message);
res.put(SynchronizerEntitySet.CREATION_DATE, this.syncConf.getCreated());
res.put(SynchronizerEntitySet.MODIFICATION_DATE, this.syncConf.getModified());
res.put(SynchronizerEntitySet.SERVICE_URL, this.syncConf.getConfig("service_uri"));
res.put(SynchronizerEntitySet.SERVICE_LOGIN, this.syncConf.getConfig("service_username"));
res.put(SynchronizerEntitySet.SERVICE_PASSWORD,
this.syncConf.getConfig("service_password") != null? "***": null);
res.put(SynchronizerEntitySet.REMOTE_INCOMING,
this.syncConf.getConfig("remote_incoming_path"));
String page_size = this.syncConf.getConfig("page_size");
res.put(SynchronizerEntitySet.PAGE_SIZE, page_size != null? Integer.decode(page_size): 30);
String copy_prod = this.syncConf.getConfig("copy_product");
res.put(SynchronizerEntitySet.COPY_PRODUCT,
copy_prod != null ? Boolean.valueOf(copy_prod): false);
res.put(SynchronizerEntitySet.FILTER_PARAM, this.syncConf.getConfig("filter_param"));
res.put(SynchronizerEntitySet.SOURCE_COLLECTION, this.syncConf.getConfig("source_collection"));
String last_created = this.syncConf.getConfig ("last_created");
if (last_created != null)
{
res.put(SynchronizerEntitySet.LAST_INGESTION_DATE, new Date (Long.decode (last_created)));
}
return res;
}
@Override
public Object getProperty (String prop_name) throws ODataException
{
Objects.requireNonNull (prop_name);
SynchronizerStatus ss = SYNCHRONIZER_SERVICE.getStatus (this.syncConf);
if (prop_name.equals(SynchronizerEntitySet.ID))
{
return this.syncConf.getId ();
}
if (prop_name.equals(SynchronizerEntitySet.LABEL))
{
return this.syncConf.getLabel ();
}
if (prop_name.equals(SynchronizerEntitySet.SCHEDULE))
{
return this.syncConf.getCronExpression ();
}
if (prop_name.equals(SynchronizerEntitySet.REQUEST))
{
return this.syncConf.getActive () ? "start" : "stop";
}
if (prop_name.equals(SynchronizerEntitySet.STATUS))
{
return ss.status.toString ();
}
if (prop_name.equals(SynchronizerEntitySet.STATUS_DATE))
{
return ss.since;
}
if (prop_name.equals(SynchronizerEntitySet.STATUS_MESSAGE))
{
return ss.message;
}
if (prop_name.equals(SynchronizerEntitySet.CREATION_DATE))
{
return this.syncConf.getCreated ();
}
if (prop_name.equals(SynchronizerEntitySet.MODIFICATION_DATE))
{
return this.syncConf.getModified ();
}
if (prop_name.equals(SynchronizerEntitySet.SERVICE_URL))
{
return this.syncConf.getConfig ("service_uri");
}
if (prop_name.equals(SynchronizerEntitySet.SERVICE_LOGIN))
{
return this.syncConf.getConfig ("service_username");
}
if (prop_name.equals(SynchronizerEntitySet.SERVICE_PASSWORD))
{
return this.syncConf.getConfig ("service_password") != null ? "***"
: null;
}
if (prop_name.equals(SynchronizerEntitySet.REMOTE_INCOMING))
{
return this.syncConf.getConfig ("remote_incoming_path");
}
if (prop_name.equals(SynchronizerEntitySet.PAGE_SIZE))
{
String val = this.syncConf.getConfig("page_size");
return val != null ? Integer.decode(val): 30;
}
if (prop_name.equals(SynchronizerEntitySet.LAST_INGESTION_DATE))
{
return new Date (
Long.decode (this.syncConf.getConfig ("last_created")));
}
if (prop_name.equals(SynchronizerEntitySet.COPY_PRODUCT))
{
String val = this.syncConf.getConfig("copy_product");
return val != null ? Boolean.parseBoolean(val): false;
}
if (prop_name.equals(SynchronizerEntitySet.FILTER_PARAM))
{
return this.syncConf.getConfig("filter_param");
}
if (prop_name.equals(SynchronizerEntitySet.SOURCE_COLLECTION))
{
return this.syncConf.getConfig("source_collection");
}
throw new ODataException ("Unknown property " + prop_name);
}
@Override
public Object navigate(NavigationSegment ns) throws ODataException
{
Object res;
if (ns.getEntitySet().getName().equals(Model.COLLECTION.getName()))
{
res = getTargetCollection();
if (res == null)
{
throw new NoTargetException(SynchronizerEntitySet.TARGET_COLLECTION);
}
}
else
{
throw new InvalidTargetException(this.getClass().getSimpleName(), ns.getEntitySet().getName());
}
return res;
}
private static boolean editsTargetCollection(ODataEntry entry) throws ODataException
{
List<String> nll = entry.getMetadata().getAssociationUris(SynchronizerEntitySet.TARGET_COLLECTION);
return !(nll == null || nll.isEmpty());
}
private static Collection getTargetCollection(ODataEntry entry)
throws ODataException
{
String navLinkName = SynchronizerEntitySet.TARGET_COLLECTION;
List<String> nll = entry.getMetadata ().getAssociationUris (navLinkName);
if (nll != null && !nll.isEmpty ())
{
if (nll.size () > 1)
{
throw new ODataException (
"A synchronizer accepts only one collection");
}
String uri = nll.get(0);
// Nullifying
if (uri == null || uri.isEmpty())
{
return null;
}
Edm edm = RuntimeDelegate.createEdm(new Model());
UriParser urip = RuntimeDelegate.getUriParser (edm);
List<PathSegment> path_segments = new ArrayList<> ();
StringTokenizer st = new StringTokenizer (uri, "/");
while (st.hasMoreTokens ())
{
path_segments.add (UriParser.createPathSegment (st.nextToken (),
null));
}
UriInfo uinfo =
urip
.parse (path_segments, Collections.<String, String> emptyMap ());
EdmEntitySet sync_ees = uinfo.getStartEntitySet ();
KeyPredicate kp = uinfo.getKeyPredicates ().get (0);
List<NavigationSegment> ns_l = uinfo.getNavigationSegments ();
Collection c = Navigator.<Collection>navigate(sync_ees, kp, ns_l, Collection.class);
return c;
}
return null;
}
/**
* If `value` is null or empty, remove `key` from the configuration.
* @param key of config entry to update.
* @param value to set, if null its entry will be removed from the config.
*/
private void updateNullableProperty(final String key, final String value)
{
if (value == null || value.isEmpty())
{
this.syncConf.removeConfig(key);
}
else
{
this.syncConf.setConfig(key, value);
}
}
}