TranslationKeyService.java
package sk.iway.iwcm.components.translation_keys.rest;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.support.PagedListHolder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import sk.iway.iwcm.Adminlog;
import sk.iway.iwcm.Cache;
import sk.iway.iwcm.Constants;
import sk.iway.iwcm.DB;
import sk.iway.iwcm.Identity;
import sk.iway.iwcm.Logger;
import sk.iway.iwcm.Tools;
import sk.iway.iwcm.components.translation_keys.jpa.TranslationKeyComparator;
import sk.iway.iwcm.components.translation_keys.jpa.TranslationKeyEntity;
import sk.iway.iwcm.components.translation_keys.jpa.TranslationKeyRepository;
import sk.iway.iwcm.doc.SearchAction;
import sk.iway.iwcm.i18n.IwayProperties;
import sk.iway.iwcm.i18n.Prop;
import sk.iway.iwcm.i18n.PropDB;
import sk.iway.iwcm.system.datatable.DatatableRestControllerV2;
import sk.iway.iwcm.users.UsersDB;
import sk.iway.iwcm.utils.Pair;
/**
* TranslationKeyService
* <p>
* Trieda sluzi na CRUD operacie pre prekladove kluce. Pracuje s dvoma zdrojmi, properties subor a tabulka
* webjet_properties. DefaultKeys su nemenne prekladove kluce, ktore sluzia iba na READ funkcionalitu.
* ChangedKeys su prekladove kluce pre ktore funguju CREATE, READ, UPDATE a aj DELETE funkcionality.
*/
@Service
public class TranslationKeyService {
private TranslationKeyRepository translationKeyRepository;
private TranslationKeyComparator translationKeyComparator;
//Represent maximum of support language translations (10 languages)
private static final char LAST_ALPHABET = 'J';
@Autowired
TranslationKeyService(TranslationKeyRepository translationKeyRepository, TranslationKeyComparator translationKeyComparator) {
//Logger.debug(TranslationKeyService.class, "TranslationKeyService.constructor v2, translationKeyRepository="+translationKeyRepository+" translationKeyComparator="+translationKeyComparator);
this.translationKeyRepository = translationKeyRepository;
this.translationKeyComparator = translationKeyComparator;
}
/**
* Returns map of language-field combination based on languages conf. property. Eg.:
* sk->A (as fieldA)
* en->B (as fieldB)
* @return
*/
public Map<String, String> getLanguageFieldCombination() {
Map<String, String> languageFieldCombination = new LinkedHashMap<>();
//Set combination of "language shortcut" and "field alphabet"
String[] lngArr = Constants.getArray("languages");
for(int i = 0; i < lngArr.length; i++) {
char fieldAlphabet = (char)(('A') + i);
//Support only certain number of languages
if(fieldAlphabet > LAST_ALPHABET) break;
languageFieldCombination.put(lngArr[i], fieldAlphabet + "");
}
return languageFieldCombination;
}
/**
* Prepare and return List od TranslationKeyEntities.
* Every entity contain all value-language combinations for specific translation key.
* This value in various languages are set in custom-fields. Every entity allso have set editorFields, that set up custom-fields
* type, name, label etc.
*
* @return lis of entities ready to by used as page data for Datable TranslationKeys
*/
private List<TranslationKeyEntity> getAllData(Identity user, String searchText) {
//List<TranslationKeyEntity> defaultLngKeys = getDefaultLngKeys(request);
Map<String, String> languageFieldCombination = getLanguageFieldCombination();
Map<String, TranslationKeyEntity> allDataMap = new HashMap<>();
//odfiltruj zoznam podla prav
boolean enableAllKeys = false;
String propertiesEnabledKeys = Constants.getStringExecuteMacro("propertiesEnabledKeys");
if (Tools.isEmpty(propertiesEnabledKeys) || user.isEnabledItem("prop.show_all_texts")) enableAllKeys = true;
String[] enabledKeys = Tools.getTokens(propertiesEnabledKeys, ",");
//prejdi vsetky jazyky a napln mapu
for (Map.Entry<String, String> lngField : languageFieldCombination.entrySet()) {
String lng = lngField.getKey();
String fieldAlphabet = lngField.getValue();
//ziskaj prekladove texty pre jazyk
//Logger.debug(getClass(), "getRes, lng="+lng);
IwayProperties prop = Prop.getInstance(lng).getProperties();
IwayProperties defaultKeys = Prop.getDefaulProperties(lng, lng, Constants.getServletContext());
for (Map.Entry<String, String> propEntry : prop.entrySet()) {
String key = propEntry.getKey();
String text = propEntry.getValue();
String originalText = defaultKeys.get(key);
TranslationKeyEntity entity = allDataMap.get(key);
if (entity == null) {
//prava staci kontrolovat len pri pridani, inak ak je uz pridana musi byt spravne pravo
if (enableAllKeys==false && PropDB.isKeyVisibleToUser(user, enabledKeys, key)==false) continue;
entity = new TranslationKeyEntity();
entity.setKey(key);
Date lastUpdate = getLastUpdateDate(key);
if (lastUpdate != null) entity.setUpdateDate(lastUpdate);
allDataMap.put(key, entity);
}
BeanWrapperImpl bw = new BeanWrapperImpl(entity);
bw.setPropertyValue("field" + fieldAlphabet, text);
bw.setPropertyValue("originalValue"+fieldAlphabet, originalText);
}
}
//sortni to podla abecedy a nastav IDecka
List<TranslationKeyEntity> allData = allDataMap.values().stream().sorted(Comparator.comparing(TranslationKeyEntity::getKey)).collect(Collectors.toList());
for (int i=0; i<allData.size(); i++) {
TranslationKeyEntity entity = allData.get(i);
entity.setId(Long.valueOf((i+1)));
}
if(Tools.isNotEmpty(searchText)) {
allData = searchAllFiltering(searchText, allData);
}
return allData;
}
/**
* Method will sort List of TranslationKeyEntities from input based on sorting pair.
*
* @param sortPair Pair<String, String> Pair containing the column name and sorting method (asc/desc).
* @param translationKeys list of entities to sort.
*/
private void sortTranslationKeys(Pair<String, String> sortPair, List<TranslationKeyEntity> translationKeys) {
if (sortPair.getSecond().equals("asc"))
translationKeys.sort(translationKeyComparator.getSortingComparator(sortPair.getFirst()));
else if (sortPair.getSecond().equals("desc"))
translationKeys.sort(translationKeyComparator.getSortingComparator(sortPair.getFirst()).reversed());
}
/**
* Return SORTED list of TranslationKeyEntities that are prepared for Datatable TranslationKey's.
*
* @return List<TranslationKeyEntity>
*/
private List<TranslationKeyEntity> getSortedTranslationKeys(HttpServletRequest request) {
String searchText = null;
if(Tools.getBooleanValue(request.getParameter("isSearchVersion"), false)) {
searchText = Tools.getStringValue(request.getParameter("searchText"), null);
}
List<TranslationKeyEntity> translationKeys = getAllData(UsersDB.getCurrentUser(request), searchText);
String sort = request.getParameter("sort");
if (sort != null) {
String[] sortArray = sort.split(",");
Pair<String, String> sortPair = new Pair<>(sortArray[0], sortArray[1]);
sortTranslationKeys(sortPair, translationKeys);
}
return translationKeys;
}
/**
* Return prepared PAGE for Datatable TranslationKey's. As input data use method "getSortedTranslationKeys".
* Prepage page with this data and allso set pagination.
*
* @param pageable pagination param for page.
* @return page for Datatable TranslationKey's with sorted translation key's and set pagination.
*/
public Page<TranslationKeyEntity> getTranslationKeys(HttpServletRequest request, Pageable pageable) {
List<TranslationKeyEntity> translationKeys = getSortedTranslationKeys(request);
PagedListHolder<TranslationKeyEntity> holder = new PagedListHolder<>(translationKeys);
holder.setPageSize(pageable.getPageSize());
holder.setPage(pageable.getPageNumber());
return new PageImpl<>(holder.getPageList(), pageable, translationKeys.size());
}
/**
* Method will handle create/edit process of TranslationKeyEntities for SINGLE language defined in entity.
* @param user
* @param entity
* @param reloadPropDB - if true the properties cache will be reloaded after save
* @return
*/
public TranslationKeyEntity createOrEditTranslationKeySingleLanguage(Identity user, TranslationKeyEntity entity, boolean reloadPropDB) {
if (PropDB.canEdit(user, entity.getKey()) == false)
throw new IllegalArgumentException(Prop.getInstance().getText("components.translation_key.cantEditThisKey"));
Date updateDate = new Date();
String key = entity.getKey();
String lng = entity.getLng();
String value = entity.getValue();
TranslationKeyEntity dbEntity = translationKeyRepository.findByKeyAndLng(key, lng);
String oldValue = null;
if (dbEntity == null) {
dbEntity = new TranslationKeyEntity();
dbEntity.setKey(key);
dbEntity.setLng(lng);
} else {
oldValue = dbEntity.getValue();
}
String newValue = PropDB.escapeUnsafeValue(user, lng, key, value);
if (newValue == null) newValue = "";
if (oldValue == null || newValue.equals(oldValue)==false) {
dbEntity.setValue(newValue);
dbEntity.setUpdateDate(updateDate);
translationKeyRepository.save(dbEntity);
}
if (reloadPropDB) {
//reloadni textove kluce
Prop.getInstance(true);
}
return dbEntity;
}
/**
* Method will handle create/edit process of TranslationKeyEntities for every supported language.
*
* CASE 1, if translation key is not in file or DB, then new DB record will by set (representing new translation key).
* CASE 2, if translation key is only in file, then new DB record will by set (representing new value of translation key),
* even if old value in file and value from EDITOR are same.
* CASE 3, if translation key have DB value, this value will be updated.
*
* !! Every translation key value in specific language is saved in DB or file like stand alone record.
*
* @param user actually logged user
* @param entity entity representing translation key and his values in all supported languages
* @return same entity that was entered
*/
public TranslationKeyEntity createOrEditTranslationKey(Identity user, TranslationKeyEntity entity, boolean isImport, String importMode, Set<String> importedColumns) {
if (PropDB.canEdit(user, entity.getKey()) == false)
throw new IllegalArgumentException(Prop.getInstance().getText("components.translation_key.cantEditThisKey"));
Date updateDate = new Date();
String key = entity.getKey();
BeanWrapper bw = new BeanWrapperImpl(entity);
//There are allready some lng translations for this key
//We must reuse ID's of allready existed entities (just update "updateDate" and "value")
for(Map.Entry<String, String> entry : getLanguageFieldCombination().entrySet()) {
String lng = entry.getKey();
String field = entry.getValue();
//skip language not in imported excel
if (isImport && importedColumns!=null && importedColumns.contains("field"+field)==false) continue;
String value = (String)bw.getPropertyValue("field"+field);
TranslationKeyEntity dbEntity = null;
if (isImport) {
//pri importe pouzivame cache
dbEntity = getCachedEntity(key, lng);
} else {
dbEntity = translationKeyRepository.findByKeyAndLng(key, lng);
}
//if entity exist and we are importig ONLY NEW keys, skip
if (isImport && "onlyNew".equals(importMode) && dbEntity!=null) {
Logger.debug(getClass(), "createOrEditTranslationKey, skipping key="+key+" lng="+lng+" value="+value+" because of importMode=onlyNew");
continue;
}
String oldValue = null;
if (dbEntity == null) {
//ak kluc este nie je v DB a hodnota je prazdna preskoc
if (Tools.isEmpty(value)) continue;
dbEntity = new TranslationKeyEntity();
dbEntity.setKey(key);
dbEntity.setLng(lng);
} else {
oldValue = dbEntity.getValue();
}
String newValue = PropDB.escapeUnsafeValue(user, lng, key, value);
if (newValue == null) newValue = "";
//ak obe nie su zadane preskoc (asi prazdny stlpec napr. spanielsky v exceli)
if (Tools.isEmpty(oldValue) && Tools.isEmpty(newValue)) continue;
if (oldValue == null || newValue.equals(oldValue)==false) {
dbEntity.setValue(newValue);
dbEntity.setUpdateDate(updateDate);
translationKeyRepository.save(dbEntity);
//auditujeme kazdy zaznam len ked to nie je import, import sa audituje v RestController v afterImportChunk
if (isImport==false) Adminlog.add(Adminlog.TYPE_PROP_UPDATE, "UPDATE:\nid: "+dbEntity.getId()+"\nprop_key: "+dbEntity.getKey()+"\nvalue:"+newValue+"\nlng:"+lng, dbEntity.getId().intValue(), -1);
if (oldValue==null) setCachedEntity(dbEntity.getKey(), dbEntity.getLng(), dbEntity);
}
}
//reloadni textove kluce
if (isImport==false) {
Prop.getInstance(true);
//reload caches, so there will not be concurrentModificationException
for(Map.Entry<String, String> entry : getLanguageFieldCombination().entrySet()) {
String lng = entry.getKey();
Prop.getInstance(lng);
}
}
//aktualizuj date cache
setLastUpdateDate(key, updateDate);
return entity;
}
/**
* Method will check is user have rights to delete entered entity. If yes, entity will be deleted. If no, throw IllegalArgumentException.
* @param user actualy logged user
* @param translationKey translation key entity we want to delete
* @param lng language variant of translation key we want delete
* @return number of deleted entities
*/
public Long delete(Identity user, String key, String lng) {
if (PropDB.canEdit(user, key) == false)
throw new IllegalArgumentException(Prop.getInstance().getText("components.translation_key.cantEditThisKey"));
setLastUpdateDate(key, null);
String oldValue = Prop.getInstance(lng).getText(key);
Long id = translationKeyRepository.deleteByKeyAndLng(key, lng);
int idInt = -1;
if (id != null) idInt = id.intValue();
Adminlog.add(Adminlog.TYPE_PROP_DELETE, "DELETE:\nid: "+id+"\nprop_key: "+key+"\n:value:"+oldValue+"\nlng:"+lng, idInt, -1);
//reloadni textove kluce
Prop.getInstance(true);
return id;
}
/**
* Metoda sluzi na filtrovanie TranslationKeyEntit.
*
* @param searchMap Map<String, String> Parametre pouzite pri filtrovani TranslationKeyProperties.
* @param sortPair Pair<String, String> Pair v ktorom sa nachadza meno stlpca a sposob sortovana (asc/desc).
* @param pageable Pageable
* @param request HttpServletRequest
* @return Page<TranslationKeyEntity>
*/
Page<TranslationKeyEntity> getFilteredTranslationKeys(Map<String, String> searchMap, Pair<String, String> sortPair, Pageable pageable, HttpServletRequest request) {
String searchText = null;
if(Tools.getBooleanValue(request.getParameter("isSearchVersion"), false)) {
searchText = Tools.getStringValue(request.getParameter("searchText"), null);
searchMap.remove("text");
}
List<TranslationKeyEntity> translationKeys = getAllData(UsersDB.getCurrentUser(request), searchText);
List<TranslationKeyEntity> filteredTranslationKeys = new ArrayList<>();
int searchMapSize = searchMap.size();
for (TranslationKeyEntity entity : translationKeys) {
BeanWrapper beanWrapper = new BeanWrapperImpl(entity);
//we are using AND between conditions, so we must count
int presentCount = 0;
for (Map.Entry<String, String> searchParameter : searchMap.entrySet()) {
if (searchParameter.getKey().equals("updateDate")) {
Pair<Date, Date> datePair = getCleanUpdateDateValue(searchParameter.getValue());
if (null != entity.getUpdateDate() && entity.getUpdateDate().after(datePair.first) && entity.getUpdateDate().before(datePair.second)) {
presentCount++;
}
} else {
String entityValue = (String) beanWrapper.getPropertyValue(searchParameter.getKey());
if (null == entityValue) break;
String value = searchParameter.getValue().toLowerCase();
entityValue = entityValue.toLowerCase();
if (
(value.startsWith("^") && value.endsWith("$") && entityValue.equals(value.substring(1, value.length()-1))) ||
(value.startsWith("^") && entityValue.startsWith(value.substring(1))) ||
(value.endsWith("$") && entityValue.endsWith(value.substring(0, value.length()-1))) ||
(entityValue.contains(value))
) presentCount++;
}
}
//we are using AND, so count must match searchMap.size
if (presentCount==searchMapSize) filteredTranslationKeys.add(entity);
}
sortTranslationKeys(sortPair, filteredTranslationKeys);
PagedListHolder<TranslationKeyEntity> holder = new PagedListHolder<>(filteredTranslationKeys);
holder.setPage(pageable.getPageNumber());
holder.setPageSize(pageable.getPageSize());
return new PageImpl<>(holder.getPageList(), pageable, filteredTranslationKeys.size());
}
/**
* Metoda sa pouziva na rozpoznanie search parametrov, ktore sa pouzivaju pri filtrovani.
*
* @param param String parameter.
* @return boolean
*/
boolean checkSearchParam(String param) {
return param.startsWith("search");
}
/**
* Metoda sa pouziva na rozpoznanie sort parametra, ktory sa pouziva pri sortovani.
*
* @param param String parameter.
* @return boolean
*/
boolean checkSortParam(String param) {
return param.equals("sort");
}
/**
* Metoda sluzi na vratenie dvojice datumov od/do.
*
* @param updateDate String skala akceptovatelnych datumov.
* @return Pair<Date, Date>
*/
private Pair<Date, Date> getCleanUpdateDateValue(String updateDate) {
String filteredDate = updateDate.replace("daterange:", "");
String[] stringDateArray = new String[2];
String[] values = Tools.getTokens(filteredDate, "-");
if (values.length==2) {
stringDateArray = values;
} else if (values.length==1) {
//ked nemame from pride to ako: daterange:-1589666400000
if (updateDate.contains("range:-")) {
stringDateArray[0] = "";
stringDateArray[1] = filteredDate.replace("-", "");
} else {
stringDateArray[0] = filteredDate.replace("-", "");
stringDateArray[1] = "";
}
}
long[] longDateArray = new long[]{Tools.getLongValue(stringDateArray[0], 0), Tools.getLongValue(stringDateArray[1], new Date().getTime())};
Date dateFrom = new Date(longDateArray[0]);
Date dateTo = new Date(longDateArray[1]);
return new Pair<>(dateFrom, dateTo);
}
/**
* Method will return TranslationKeyRepository set in gloval variable of this Service.
*
* @return TranslationKeyRepository
*/
public TranslationKeyRepository getTranslationKeyRepository() { return translationKeyRepository; }
/**
* Return cached map od key-lastUpdateDate
* @return
*/
private Map<String, Date> getUpdateDateCache() {
String cacheKey = "TranslationKeyService.updateDateMap";
Cache c = Cache.getInstance();
@SuppressWarnings("unchecked")
Map<String, Date> updateMap = (Map<String, Date>)c.getObject(cacheKey);
if (updateMap == null) {
//musime mapu naplnit z DB
updateMap = new Hashtable<>();
List<TranslationKeyEntity> all = translationKeyRepository.findAllByOrderByUpdateDateAsc();
for (TranslationKeyEntity entity : all) {
if (entity.getUpdateDate()!=null) updateMap.put(entity.getKey(), entity.getUpdateDate());
}
c.setObjectSeconds(cacheKey, updateMap, 10*60, true);
}
return updateMap;
}
/**
* Returns last update date for key from cache
* @param key
* @return
*/
private Date getLastUpdateDate(String key) {
Map<String, Date> updateMap = getUpdateDateCache();
Date d = updateMap.get(key);
if (d==null && key.contains("&")) {
key = Tools.replace(key, "<", "<");
key = Tools.replace(key, ">", ">");
d = updateMap.get(key);
}
return d;
}
/**
* Set last update date into cache
* @param key
* @param date - date OR NULL to delete key from cache
*/
private void setLastUpdateDate(String key, Date date) {
Map<String, Date> updateMap = getUpdateDateCache();
if (date == null) updateMap.remove(key);
else updateMap.put(key, date);
}
private TranslationKeyEntity getCachedEntity(String key, String language) {
return getRowIdCache().get(getRowIdKey(key, language));
}
private void setCachedEntity(String key, String language, TranslationKeyEntity entity) {
getRowIdCache().put(getRowIdKey(key, language), entity);
}
/**
* For import of large file it's faster to have local Map of all keys instead of lookup it in DB for every row
* @return
*/
private Map<String, TranslationKeyEntity> getRowIdCache() {
String CACHE_KEY = "TranslationKeyService.rowIdCache";
Cache c = Cache.getInstance();
@SuppressWarnings("unchecked")
Map<String, TranslationKeyEntity> rowIdMap = (Map<String, TranslationKeyEntity>)c.getObject(CACHE_KEY);
if (DatatableRestControllerV2.getLastImportedRow()!=null && DatatableRestControllerV2.getLastImportedRow().intValue()==1) {
//reset cache on first row
rowIdMap = null;
}
if (rowIdMap != null) return rowIdMap;
rowIdMap = new HashMap<>();
List<TranslationKeyEntity> allItems = translationKeyRepository.findAll();
for (TranslationKeyEntity e : allItems) {
rowIdMap.put(getRowIdKey(e.getKey(), e.getLng()), e);
}
c.setObject(CACHE_KEY, rowIdMap, 10);
return rowIdMap;
}
private String getRowIdKey(String key, String language) {
return key+"="+language;
}
/**
* Simple method to save translation
* @param key
* @param translation
* @param lng
*/
public TranslationKeyEntity saveTranslation(String key, String translation, String lng) {
TranslationKeyEntity entity = translationKeyRepository.findByKeyAndLng(key, lng);
if (entity == null) {
entity = new TranslationKeyEntity();
entity.setKey(key);
}
entity.setUpdateDate(new Date(Tools.getNow()));
entity.setValue(translation);
entity.setLng(lng);
translationKeyRepository.save(entity);
//Prop.getInstance(Constants.getServletContext(), lng, true);
Prop.getInstance(true);
Prop.deleteMissingText(key, lng);
return entity;
}
private List<TranslationKeyEntity> searchAllFiltering(String text, List<TranslationKeyEntity> allData) {
List<TranslationKeyEntity> filtered = new ArrayList<>();
if(Tools.isEmpty(text)) return filtered;
// Prepare search text
String cmpText = DB.internationalToEnglish(text.toLowerCase());
for(TranslationKeyEntity entity : allData) {
// Prepare and check key
String cmpPropKey = DB.internationalToEnglish(entity.getKey().toLowerCase());
if(cmpPropKey.contains(cmpText)) {
filtered.add(entity);
continue;
}
// Prepare and check every language field
char lastAlphabet = 'J';
for (char alphabet = 'A'; alphabet <= lastAlphabet; alphabet++) {
String propValue = entity.getOriginalValue(alphabet);
if(Tools.isEmpty(propValue)) continue;
String cmpPropValue = DB.internationalToEnglish(propValue.toLowerCase());
if(SearchAction.containsIgnoreHtml(cmpPropValue, cmpText)) {
filtered.add(entity);
break;
}
}
}
return filtered;
}
}