SyncDirAction.java

package sk.iway.iwcm.stripes;

import java.beans.XMLDecoder;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.ConcurrentModificationException;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.beanutils.BeanUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.LoggerFactory;

import net.sourceforge.stripes.action.ActionBeanContext;
import net.sourceforge.stripes.action.ForwardResolution;
import net.sourceforge.stripes.action.Resolution;
import sk.iway.iwcm.Constants;
import sk.iway.iwcm.DBPool;
import sk.iway.iwcm.FileTools;
import sk.iway.iwcm.Identity;
import sk.iway.iwcm.Logger;
import sk.iway.iwcm.PathFilter;
import sk.iway.iwcm.Tools;
import sk.iway.iwcm.common.CloudToolsForCore;
import sk.iway.iwcm.common.SearchTools;
import sk.iway.iwcm.database.SimpleQuery;
import sk.iway.iwcm.doc.DocDB;
import sk.iway.iwcm.doc.DocDetails;
import sk.iway.iwcm.doc.GroupDetails;
import sk.iway.iwcm.doc.GroupsDB;
import sk.iway.iwcm.doc.PerexGroupBean;
import sk.iway.iwcm.doc.TemplateDetails;
import sk.iway.iwcm.doc.TemplatesDB;
import sk.iway.iwcm.editor.DocNoteBean;
import sk.iway.iwcm.editor.EditorDB;
import sk.iway.iwcm.editor.EditorForm;
import sk.iway.iwcm.i18n.Prop;
import sk.iway.iwcm.io.IwcmFile;
import sk.iway.iwcm.io.IwcmInputStream;
import sk.iway.iwcm.io.IwcmOutputStream;
import sk.iway.iwcm.sync.WarningListener;
import sk.iway.iwcm.sync.export.Content;
import sk.iway.iwcm.sync.inport.BannerImporter;
import sk.iway.iwcm.sync.inport.ContentBannerBean;
import sk.iway.iwcm.sync.inport.ContentFileBean;
import sk.iway.iwcm.sync.inport.ContentGalleryBean;
import sk.iway.iwcm.sync.inport.ContentInquiryBean;
import sk.iway.iwcm.sync.inport.GalleryImporter;
import sk.iway.iwcm.sync.inport.InquiryImporter;
import sk.iway.iwcm.sync.inport.Numbered;
import sk.iway.iwcm.system.stripes.WebJETActionBean;
import sk.iway.iwcm.users.UserGroupDetails;
import sk.iway.iwcm.users.UserGroupsDB;
import sk.iway.iwcm.users.UsersDB;
import sk.iway.spirit.MediaDB;
import sk.iway.spirit.model.Media;
import sk.iway.spirit.model.MediaGroupBean;

/**
 *  SyncDirAction.java - synchronizacia adresara a web stranok zo vzdialeneho servera
 *
 *@Title        webjet4
 *@Company      Interway s.r.o. (www.interway.sk)
 *@Copyright    Interway s.r.o. (c) 2001-2007
 *@author       $Author: jeeff $
 *@version      $Revision: 1.11 $
 *@created      Date: 8.11.2007 13:37:13
 *@modified     $Date: 2010/02/09 09:02:04 $
 */
public class SyncDirAction extends WebJETActionBean
{
	private static org.slf4j.Logger log = LoggerFactory.getLogger(SyncDirAction.class);

	private int localGroupId;
	private int remoteGroupId;
	private String localDomain;

	private GroupDetails localSrcGroup;

	private List<GroupDetails> remoteGroups = null;
	private List<DocDetails> remoteDocs = null;
	private List<TemplateDetails> remoteTemps = null;
	private List<UserGroupDetails> remoteUserGroups = null;
	private List<PerexGroupBean> remotePerexGroups = null;
	private List<Media> remoteMedia = null;
	private Content remoteContent = null;

	private String remoteRootPath = null;
	private GroupDetails localRootGroup = null;

	private boolean createMissingTemplates = true;
	private boolean createMissingUserGroups = true;

	private String syncDir = null; // if null use remoteServer else use local files for sync

	private Map<Integer, DocNoteBean> remoteNotesMap = null;

	private List<String> errorMessages = new ArrayList<>();

	private String compareBy = null;
	private Map<Integer, Integer> remoteToLocalMediaGroups = null;

	/**
	 * Nacita data zo vzdialeneho servera
	 * @return
	 */
	public Resolution btnLoadData()
	{
		SyncDirWriterService.printStatusMsg("components.syncDirAction.title", true, getContext(), getRequest());

		//aby sa nam vymazala cache (ak je pouzita)
		remoteGroups = null;
		remoteDocs = null;
		remoteToLocalMediaGroups = null;
		Logger.debug(SyncDirAction.class, "btnLoadData Reading remote groups");

		try
		{
			SyncDirWriterService.printStatusMsg("components.syncDirAction.prepare_groups", false, getContext(), getRequest());
			remoteGroups = getRemoteGroups();

			SyncDirWriterService.printStatusMsg("components.syncDirAction.prepare_docs", false,  getContext(), getRequest());
			remoteDocs = getRemoteDocs();
		}
		catch (Exception e)
		{
			sk.iway.iwcm.Logger.error(e);
		}

		SyncDirWriterService.printStatusMsg("components.syncDirAction.prepare_content", false,  getContext(), getRequest());
		Logger.debug(SyncDirAction.class, "btnLoadData Remote groups="+remoteGroups);
		if (remoteGroups == null || remoteDocs == null)
		{
			context.getRequest().setAttribute("errorText", Prop.getInstance(getRequest()).getText("components.sync.errorReadingZipFile", getErrorMessagesHtml()));
		}
		remoteContent = getRemoteContent();

		return new ForwardResolution("/components/maybeError.jsp");
	}

	private void foldersSync(HttpServletRequest request, PrintWriter writer) {
		//
		Prop prop = Prop.getInstance(request);

		SyncDirWriterService.prepareProgress(prop.getText("components.syncDirAction.progress.syncingFolders"), "folderSyncCount", prop.getText("components.syncDirAction.progress.syncingFolder") + ": - / -", writer);

		Map<String, String> selectedGroupsMap = SyncDirWriterService.getOptionsMap("group_", request);
		if(selectedGroupsMap.size() < 1) return;

		int groupsToPrepareCount = 0;
		int preparedGroupsCount = 1;
		List<GroupDetails> remoteGroupsToSync = getRemoteGroups();

		//Count groups to sync
		groupsToPrepareCount = (int) remoteGroupsToSync.stream()
			.filter(remoteGroup -> selectedGroupsMap.get("group_" + remoteGroup.getGroupId()) != null)
			.count();


		for (GroupDetails remoteGroup : remoteGroupsToSync)
		{
			if (selectedGroupsMap.get("group_" + remoteGroup.getGroupId()) != null)
			{
				SyncDirWriterService.updateProgress("folderSyncCount", prop.getText("components.syncDirAction.progress.syncingFolder") + ": " + preparedGroupsCount + " / " + groupsToPrepareCount, writer);
				preparedGroupsCount++;

				createLocalGroup(remoteGroup);
			}
		}
	}

	private List<Integer> pagesSync(HttpServletRequest request, PrintWriter writer) {
		Prop prop = Prop.getInstance(request);
		//
		SyncDirWriterService.prepareProgress(prop.getText("components.syncDirAction.progress.syncingDocs"), "webPageSyncCount", prop.getText("components.syncDirAction.progress.syncingDoc") + ": - / -", writer);

		List<Integer> historyIds = new ArrayList<>();
		Map<String, String> selectedDocsMap = SyncDirWriterService.getOptionsMap("doc_", request);
		if(selectedDocsMap.size() < 1) return historyIds;

		List<DocDetails> remoteDocToSync = getRemoteDocs();
		int pagesToPrepareCount = 0;
		int preparedPagesCount = 1;

		//Count pages to prepare
		pagesToPrepareCount = (int) remoteDocToSync.stream()
			.filter(remoteDoc -> selectedDocsMap.get("doc_" + remoteDoc.getDocId()) != null)
			.count();


		for (DocDetails remoteDoc : remoteDocToSync)
		{
			if (selectedDocsMap.get("doc_" + remoteDoc.getDocId()) != null)
			{
				SyncDirWriterService.updateProgress("webPageSyncCount", prop.getText("components.syncDirAction.progress.syncingDoc") + ": " + preparedPagesCount + " / " + pagesToPrepareCount, writer);
				preparedPagesCount++;

				int historyId = createLocalDoc(remoteDoc);
				historyIds.add(historyId);
			}
		}

		return historyIds;
	}

	private void importingFiles(Content content, HttpServletRequest request, PrintWriter writer) {
		Prop prop = Prop.getInstance(request);
		//
		SyncDirWriterService.prepareProgress(prop.getText("components.syncDirAction.progress.syncingFiles"), "filesImportCount", prop.getText("components.syncDirAction.progress.syncingFile") + ": - / -", writer);

		if(content == null) return;

		Map<String, String> selectedFilesMap = SyncDirWriterService.getOptionsMap("file_", request);
		if(selectedFilesMap.size() < 1) return;

		int importedFilesCount = 1;
		Iterable<Numbered<Content.File>> filesToImport = Numbered.list(content.getFiles());
		int filesToImportCount = SyncDirWriterService.getCountToHandle(selectedFilesMap, filesToImport, "file_");

		for (Numbered<Content.File> file : filesToImport)
		{
			if (selectedFilesMap.get("file_" + file.number) != null)
			{
				SyncDirWriterService.updateProgress("filesImportCount", prop.getText("components.syncDirAction.progress.syncingFile") + ": " + importedFilesCount + " / " + filesToImportCount, writer);
				importedFilesCount++;

				createLocalContentFile(file.item);
			}
		}
	}

	/**
	 * Vykonanie synchronizacie adresarov a stranok z remote servera na local
	 * @return
	 */
	public Resolution btnSync()
	{
		if (isAdminLogged()==false) return new ForwardResolution(RESOLUTION_NOT_LOGGED);

		try
		{
			HttpServletResponse response = getContext().getResponse();
        	PrintWriter writer = response.getWriter();

			HttpServletRequest request = context.getRequest();
			//UPOZORNENIE: ziadnym sposobom nemodifikovat remote objekty!
			// ak je to nutne je potrebne nasledne vratit povodne hodnoty

			GroupDetails localGroup = getLocalRootGroup();
			if (localGroup != null && Tools.isNotEmpty(localGroup.getDomainName()))
			{
				setLocalDomain(localGroup.getDomainName());
			}

			//FOLDER SYNC
			List<Integer> historyGroupsIds = new ArrayList<>();
			Date start = new Date();
			foldersSync(request, writer);
			historyGroupsIds = getNewGroups(start);

			//prepare mediaGroups mapping
			remoteToLocalMediaGroups = prepareRemoteToLocalMediaGroupMap();

			//WEB PAGE SYNC
			List<Integer> historyIds = pagesSync(request, writer);
			saveRollbackJson(historyIds, historyGroupsIds);

			//FILE IMPORT
			Content content = getRemoteContent();
			importingFiles(content, request, writer);

			// import komponentov
			BannerImporter.importBanners(request, content, writer);
			InquiryImporter.importInquiries(request, content, writer);
			GalleryImporter.importGalleries(request, content, writer);
		}
		catch (Exception ex)
		{
			sk.iway.iwcm.Logger.error(ex);
			getRequest().setAttribute("errorText", "ERROR: "+ex.getMessage());
		}

		if (remoteGroups == null || remoteDocs == null)
		{
			context.getRequest().setAttribute("errorText", Prop.getInstance(getRequest()).getText("components.sync.errorReadingZipFile", getErrorMessagesHtml()));
		}

		return new ForwardResolution("/components/maybeError.jsp");
	}

	private String getErrorMessagesHtml()
	{
		StringBuilder html = new StringBuilder();

		if (errorMessages.size()>0)
		{
			html.append("<ul>\n");
			for (String error : errorMessages)
			{
				html.append("<li>").append(error).append("</li>\n");
			}
			html.append("</ul>\n");
		}

		return html.toString();
	}


	/**
	 * Vytvori adresar na lokalnom serveri
	 * @param remoteGroup
	 * @return
	 */
	private boolean createLocalGroup(GroupDetails remoteGroup)
	{
		GroupsDB groupsDB = GroupsDB.getInstance();
		String localPath = getLocalPath(remoteGroup);
		GroupDetails localGroup = groupsDB.getGroupByPath(localPath);
		if (localGroup == null)
		{
			//treba ulozit, ziskaj parenta
			int i = localPath.lastIndexOf('/');
			if (i>0)
			{
				String localParentPath = localPath.substring(0, i);
				Logger.debug(SyncDirAction.class, "localPath: "+localPath+" localParentPath: "+localParentPath+" remotePath: "+remoteGroup.getFullPath());
				GroupDetails localParentGroup = groupsDB.getCreateGroup(localParentPath);
				if (localParentGroup!=null)
				{
					//uprav mapovanie user group skupin
					String localUserGroups = convertUserGroupIdsToLocal(remoteGroup);
					if (localUserGroups==null) return false;

					int oGroupId = remoteGroup.getGroupId();
					int oParentGroupId = remoteGroup.getParentGroupId();
					int oTempId = remoteGroup.getTempId();
					String oUserGroups = remoteGroup.getPasswordProtected();
					String oFullPath = remoteGroup.getFullPath();
					String oNavbar = null;
					String oDomain = remoteGroup.getDomainName();
					int oDefaultDocId = remoteGroup.getDefaultDocId();

					remoteGroup.setGroupId(-1);
					remoteGroup.setParentGroupId(localParentGroup.getGroupId());
					if (remoteGroup.getNavbarName().indexOf("href=")!=-1)
					{
						//uz prenasame navbarName, kvoli spetnej kompatibilite ale mame tento backfix
						oNavbar = remoteGroup.getNavbar();
						remoteGroup.setNavbar(SearchTools.htmlToPlain(oNavbar));
					}
					remoteGroup.setDefaultDocId(-1);

					remoteGroup.setTempId(localRootGroup.getTempId());
					if (createMissingTemplates)
					{
						//uprav mapovanie sablon
						int localTempId = getCreateTemplate(oTempId);
						Logger.debug(SyncDirAction.class, "Setting localTempId to:"+localTempId+" for group "+remoteGroup.getGroupIdName());
						if (localTempId>0) remoteGroup.setTempId(localTempId);
					}
					remoteGroup.setPasswordProtected(localUserGroups);
					if(Tools.isNotEmpty(localDomain))
						remoteGroup.setDomainName(localDomain);

					if("cloud".equals(Constants.getInstallName()))
					{
						String lng = getLocalLng();
						if (Tools.isNotEmpty(lng))
						{
							Logger.debug(getClass(), "Cloud, translating to: " + lng);
							remoteGroup.setGroupName(CloudToolsForCore.translate(lng, remoteGroup.getGroupName()));
							remoteGroup.setNavbar(SearchTools.htmlToPlain(CloudToolsForCore.translate(lng, remoteGroup.getNavbar())));
							remoteGroup.setUrlDirName(CloudToolsForCore.translate(lng, remoteGroup.getGroupName()));
						}
					}

					groupsDB.setGroup(remoteGroup);

					//vrat do povodneho stavu
					remoteGroup.setGroupId(oGroupId);
					remoteGroup.setParentGroupId(oParentGroupId);
					remoteGroup.setTempId(oTempId);
					remoteGroup.setPasswordProtected(oUserGroups);
					remoteGroup.setFullPath(oFullPath);
					if (oNavbar != null) remoteGroup.setNavbar(oNavbar);
					remoteGroup.setDomainName(oDomain);
					remoteGroup.setDefaultDocId(oDefaultDocId);

					return true;
				}
			}
		}
		else
		{
			//chceme prepisat data, toto musi zostat zachovane
			int groupId = localGroup.getGroupId();
			int parentGroupId = localGroup.getParentGroupId();
			int defaultDocId = localGroup.getDefaultDocId();
			int tempId = localGroup.getTempId();
			String domainName = localGroup.getDomainName();
			String userGroups = localGroup.getPasswordProtected();
			try
			{
			    //skopiruj data
				BeanUtils.copyProperties(localGroup, remoteGroup);

				//obnov zachovane data
				localGroup.setGroupId(groupId);
				localGroup.setParentGroupId(parentGroupId);
				localGroup.setDefaultDocId(defaultDocId);
                if (createMissingTemplates) localGroup.setTempId(getCreateTemplate(remoteGroup.getTempId()));
                else localGroup.setTempId(tempId);
                localGroup.setDomainName(domainName);

                String localUserGroups = convertUserGroupIdsToLocal(remoteGroup);
                if (localUserGroups!=null) localGroup.setPasswordProtected(localUserGroups);
				else localGroup.setPasswordProtected(userGroups);

				groupsDB.setGroup(localGroup);
			}
			catch (Exception ex)
			{
			    sk.iway.iwcm.Logger.error(ex);
			}
		}
		return false;
	}

    /**
     * Skonvertuje ID pouzivatelskych skupin z remote grupy na lokalne IDecka, vrati NULL ak nastane chyba
     * @param remoteGroup
     * @return
     */
	private String convertUserGroupIdsToLocal(GroupDetails remoteGroup)
    {
        StringBuilder localUserGroups = null;
        if (Tools.isNotEmpty(remoteGroup.getPasswordProtected()) && createMissingUserGroups)
        {
            StringTokenizer st = new StringTokenizer(remoteGroup.getPasswordProtected(), ",");
            while (st.hasMoreTokens())
            {
                int remoteUserGroupId = Tools.getIntValue(st.nextToken(), -1);
                if (remoteUserGroupId>0)
                {
                    String remoteUserGroupName = getRemoteUserGroupName(remoteUserGroupId);
                    if (Tools.isEmpty(remoteUserGroupName))
                    {
                        context.getRequest().setAttribute("errorText", "Neexistuje remote user group "+remoteUserGroupId);
                        Logger.debug(SyncDirAction.class, "Neexistuje remote user group "+remoteUserGroupId);
                        return null;
                    }
                    int localUserGroupId = getCreateUserGroup(remoteUserGroupId);
                    if (localUserGroupId<1)
                    {
                        context.getRequest().setAttribute("errorText", "Neexistuje pouzivatelska skupina "+remoteUserGroupName);
                        Logger.debug(SyncDirAction.class, "Neexistuje pouzivatelska skupina "+remoteUserGroupName);
                        return null;
                    }
                    if (localUserGroups==null) localUserGroups = new StringBuilder(Integer.toString(localUserGroupId));
                    else localUserGroups.append(",").append(localUserGroupId);
                }
            }
        }
		if (localUserGroups==null) return "";

		return localUserGroups.toString();
	 }

	 /**
	  * Convert string of remote group ids like 40,47,57 to local group ids, filter not/yet existing
	  * @param groupIds
	  * @return
	  */
	 private String convertGroupIdsToLocalFilter(String groupIds) {
		//ak export neobsahuje skupiny, tak vrat povodne
		if (Tools.isEmpty(groupIds)) return groupIds;

		StringBuilder localGroups = new StringBuilder();
		try {
			int[] ids = Tools.getTokensInt(groupIds, ",");
			for (int remoteGroupId : ids) {
				GroupDetails localGroup = getLocalGroup(remoteGroupId);
				if (localGroup != null) {
					if (localGroups.length()>0) localGroups.append(",");
					localGroups.append(String.valueOf(localGroup.getGroupId()));
				}
			}
		} catch (Exception ex) {
			sk.iway.iwcm.Logger.error(ex);
			//ak to padlo, nastav povodne
			if (Tools.isEmpty(localGroups)) localGroups.append(groupIds);
		}
		return localGroups.toString();
	 }

	 /**
     * Skonvertuje ID perex skupin z remote doc na lokalne IDecka, vrati NULL ak nastane chyba
     * @param remoteGroup
     * @return
     */
		private String convertPerexGroupIdsToLocal(String perexGroupIds) {
			StringBuilder localPerexGroups = new StringBuilder();
			try {
				List<PerexGroupBean> perexGroups = getRemotePerexGroups();
				//ak export neobsahuje perex grupy, tak vrat povodne
				if (perexGroups == null || perexGroups.size()<1) return perexGroupIds;

				if (Tools.isNotEmpty(perexGroupIds) && createMissingUserGroups) {
					StringTokenizer st = new StringTokenizer(perexGroupIds, ",");
					while (st.hasMoreTokens()) {

						int remotePerexGroupId = Tools.getIntValue(st.nextToken(), -1);
						if (remotePerexGroupId > 0) {
							String remotePerexGroupName = getRemotePerexGroupName(remotePerexGroupId);
							if (Tools.isEmpty(remotePerexGroupName)) {
								context.getRequest().setAttribute("errorText", "Neexistuje remote perex group " + remotePerexGroupId);
								Logger.debug(SyncDirAction.class, "Neexistuje remote perex group " + remotePerexGroupId);
								continue;
							}

							int localPerexGroupId = getCreatePerexGroup(remotePerexGroupId);
							if (localPerexGroupId < 1) {
								context.getRequest().setAttribute("errorText", "Neexistuje perex skupina " + remotePerexGroupName);
								Logger.debug(SyncDirAction.class, "Neexistuje perex skupina " + remotePerexGroupName);
								//ak sa nepodarilo vytvorit, ponechaj povodne IDecko
								localPerexGroupId = remotePerexGroupId;
							}

							if (localPerexGroups.length()>0) localPerexGroups.append(",");
							localPerexGroups.append(Integer.toString(localPerexGroupId));
						}
					}
				}
			} catch (Exception ex) {
				sk.iway.iwcm.Logger.error(ex);
				//ak to padlo, nastav povodne
				if (Tools.isEmpty(localPerexGroups)) localPerexGroups.append(perexGroupIds);
			}
			return localPerexGroups.toString();
		}

	/**
	 * Vytvori stranku na lokalnom serveri
	 * @param remoteDoc
	 * @return
	 */
	private int createLocalDoc(DocDetails remoteDoc)
	{
		Logger.debug(SyncDirAction.class, "createLocalDoc, remoteDoc="+remoteDoc.getDocId()+" title="+remoteDoc.getTitle());

		GroupDetails localGroup = getLocalGroup(remoteDoc.getGroupId());
		if (localGroup==null)
		{
			//asi nezaskrtol, ze sa ma syncnut aj adresar
			createLocalGroup(getRemoteGroup(remoteDoc.getGroupId()));
			localGroup = getLocalGroup(remoteDoc.getGroupId());
		}
		if (localGroup!=null)
		{
			//najdi lokalnu verziu
			DocDetails localDoc = getLocalDoc(remoteDoc);

			//uprav mapovanie sablon!!!
			int localTempId = getCreateTemplate(remoteDoc.getTempId());
			if (localTempId<1) return -1;

			//uprav mapovanie user group skupin
			StringBuilder localUserGroups = null;
			if (Tools.isNotEmpty(remoteDoc.getPasswordProtected()))
			{
				StringTokenizer st = new StringTokenizer(remoteDoc.getPasswordProtected(), ",");
				while (st.hasMoreTokens())
				{
					int remoteUserGroupId = Tools.getIntValue(st.nextToken(), -1);
					if (remoteUserGroupId>0)
					{
						int localUserGroupId = getCreateUserGroup(remoteUserGroupId);
						if (localUserGroupId<1)
						{
							return -1;
						}
						if (localUserGroups==null) localUserGroups = new StringBuilder(Integer.toString(localUserGroupId));
						else localUserGroups.append(",").append(localUserGroupId);
					}
				}
			}

			EditorForm ef = new EditorForm(remoteDoc);

			Logger.debug(SyncDirAction.class, "createLocalDoc: IN="+Constants.getInstallName()+" is Cloud="+("cloud".equals(Constants.getInstallName())));
			if("cloud".equals(Constants.getInstallName()))
			{
				String lng = getLocalLng();
				Logger.debug(SyncDirAction.class, "createLocalDoc: lng="+lng);
				if (Tools.isNotEmpty(lng))
				{
					Logger.debug(getClass(), "Cloud, translating remote docid "+ remoteDoc.getDocId() +" title: "+ remoteDoc.getTitle() +" to: " + lng);
					ef = CloudToolsForCore.translate(lng, ef, localGroup);
				}
			}

			if (localDoc != null) ef.setDocId(localDoc.getDocId());
			else ef.setDocId(-1);

			ef.setData(fixGroupIdsInContent(ef.getData()));
			ef.setData(fixMediaGroupIdsInContent(ef.getData()));

			ef.setGroupId(localGroup.getGroupId());
			ef.setPublish("1");
			ef.setTempId(localTempId);
			ef.setPasswordProtectedString(Tools.toString(localUserGroups));
			ef.setPerexGroupString(convertPerexGroupIdsToLocal(remoteDoc.getPerexGroupIdsString()));

			Identity user = UsersDB.getCurrentUser(getContext());
			ef.setAuthorId(user.getUserId());

			//nastavime poznamku pre redaktora
			DocNoteBean note = getRemoteDocsNotes().get(remoteDoc.getDocId());
			if(note!=null)
			{
				ef.setNote(note.getNote());
			}

			int docid = EditorDB.saveEditorForm(ef, context.getRequest());

			EditorDB.cleanSessionData(context.getRequest());

			//skontroluj, ci remoteDoc nie je nejake default doc a nastav na local group
			fixDefaultDocId(remoteDoc, ef.getDocId());

			syncMedia(remoteDoc, ef);

			return docid;
		}
		return -1;
	}

	private void syncMedia(DocDetails remoteDoc, EditorForm ef) {
		//syncni media
		List<Media> media = getRemoteMedia();
		if (media != null)
		{
			//toto su media co uz mam
			List<Media> mediaLocal = MediaDB.getMedia(getSession(), "documents", ef.getDocId(), null, 0, false);
			//toto je set medii ktore som prepisal z remote, ostatne budem musiet premazat
			Set<String> mediaLinks = new HashSet<>();

			MediaDB mediaDB = new MediaDB();
			for (Media m : media)
			{
				if (m.getMediaFkId()!=null && m.getMediaFkId().intValue()==remoteDoc.getDocId())
				{
					Media mLocal = getMediaByLink(mediaLocal, m.getMediaLink());
					if (mLocal == null) mLocal = new Media();

					mLocal.setMediaFkId(Integer.valueOf(ef.getDocId()));
					mLocal.setMediaFkTableName(m.getMediaFkTableName());
					mLocal.setMediaTitleSk(m.getMediaTitleSk());
					mLocal.setMediaTitleCz(m.getMediaTitleCz());
					mLocal.setMediaTitleEn(m.getMediaTitleEn());
					mLocal.setMediaTitleDe(m.getMediaTitleDe());
					mLocal.setMediaLink(m.getMediaLink());
					mLocal.setMediaThumbLink(m.getMediaThumbLink());
					mLocal.setMediaGroup(m.getMediaGroup());
					mLocal.setMediaInfoSk(m.getMediaInfoSk());
					mLocal.setMediaInfoCz(m.getMediaInfoCz());
					mLocal.setMediaInfoEn(m.getMediaInfoEn());
					mLocal.setMediaInfoDe(m.getMediaInfoDe());
					mLocal.setMediaSortOrder(m.getMediaSortOrder());
					mLocal.setLastUpdate(m.getLastUpdate());

					//set groups list from m to mLocal - iterate list and find local media group by mediaGroupName
					if (m.getGroups() != null && m.getGroups().size() > 0)
					{
						List <MediaGroupBean> mediaGroups = new ArrayList<>();
						for (MediaGroupBean remoteGroup : m.getGroups())
						{
							MediaGroupBean localMediaGroup = MediaDB.getGroup(remoteGroup.getMediaGroupName());

							//not mapped? why? it should be mapped with prepareRemoteToLocalMediaGroupMap()
							if (localMediaGroup == null) continue;
							mediaGroups.add(localMediaGroup);
						}
						mLocal.setGroups(mediaGroups);
					}

					Logger.debug(SyncDirAction.class, "Saving media: "+mLocal.getMediaId()+" title="+mLocal.getMediaTitleSk()+" link="+mLocal.getMediaLink());
					mediaDB.save(mLocal);

					mediaLinks.add(m.getMediaLink());
				}
			}

			//pomaz media, ktore neboli aktualizovane
			for (Media mLocal : mediaLocal)
			{
				if (mediaLinks.contains(mLocal.getMediaLink())) continue;

				Logger.debug(SyncDirAction.class, "Deleting media: "+mLocal.getMediaId()+" title="+mLocal.getMediaTitleSk()+" link="+mLocal.getMediaLink());
				mediaDB.delete(mLocal);
			}
		}
	}

	private Media getMediaByLink(List<Media> media, String mediaLink)
	{
		for (Media m : media)
		{
			if (m.getMediaLink().equals(mediaLink)) return m;
		}
		return null;
	}

	private void fixDefaultDocId(DocDetails remoteDoc, int localDocId)
	{
		for (GroupDetails remoteGroup : getRemoteGroups())
		{
			if (remoteGroup != null && remoteGroup.getDefaultDocId()==remoteDoc.getDocId())
			{
				GroupDetails localGroup = getLocalGroup(remoteGroup);
				if (localGroup != null)
				{
					localGroup.setDefaultDocId(localDocId);
					GroupsDB.getInstance().setGroup(localGroup);
				}
			}
		}
	}

	private boolean createLocalContentFile(Content.File remoteFile)
	{
		String tempPath = Tools.getRealPath(syncDir + "/content/" + remoteFile.getZipPath());
		String localPath = Tools.getRealPath(remoteFile.getVirtualPath());
		IwcmFile tempFile = new IwcmFile(tempPath);
		IwcmFile localFile = new IwcmFile(localPath);
		try
		{
			FileTools.copyFile(tempFile, localFile);
			localFile.setLastModified(remoteFile.getTime());
			return true;
		}
		catch (Exception ex)
		{
			return false;
		}
	}

	/**
	 * Ziska ID lokalnej sablony pre ID vzdialenej sablony
	 * ak sablona neexistuje a je zvolene, ze sa ma vytvorit, tak sa vytvori
	 * @param remoteTempId
	 * @return
	 */
	public int getCreateTemplate(int remoteTempId)
	{
		if (createMissingTemplates==false) return localRootGroup.getTempId();

		TemplateDetails remoteTemp = getRemoteTemp(remoteTempId);
		if (remoteTemp==null)
		{
			context.getRequest().setAttribute("errorText", "Neexistuje remote sablona pre tempId="+remoteTempId);
			Logger.debug(SyncDirAction.class, "Neexistuje remote sablona pre tempId="+remoteTempId);
			return -1;
		}
		int localTempId = getLocalTempId(remoteTemp.getTempName());
		if (localTempId < 1 && createMissingTemplates)
		{
			int oTempId = remoteTemp.getTempId();
			String oTempName = remoteTemp.getTempName();
			String oTempLng = remoteTemp.getLng();
			int oTempHeaderDocId = remoteTemp.getHeaderDocId();
			int oTempFooterDocId = remoteTemp.getFooterDocId();
			int oTempMenuDocId = remoteTemp.getMenuDocId();
			String oAvailableGroups = remoteTemp.getAvailableGroups();

			if(remoteDocs == null) getRemoteDocs();

			//DocDetails header = selectFirst(remoteDocs,having(on(DocDetails.class).getDocId(),equalTo(remoteTemp.getHeaderDocId())));
			DocDetails header = remoteDocs.stream().filter(d -> d.getDocId() == remoteTemp.getHeaderDocId()).findFirst().orElse(null);
			//DocDetails footer = selectFirst(remoteDocs,having(on(DocDetails.class).getDocId(),equalTo(remoteTemp.getFooterDocId())));
			DocDetails footer = remoteDocs.stream().filter(d -> d.getDocId() == remoteTemp.getFooterDocId()).findFirst().orElse(null);
			//DocDetails menu = selectFirst(remoteDocs,having(on(DocDetails.class).getDocId(),equalTo(remoteTemp.getMenuDocId())));
			DocDetails menu = remoteDocs.stream().filter(d -> d.getDocId() == remoteTemp.getMenuDocId()).findFirst().orElse(null);

			//save them to global /System
			int globalHeaderFooterGroupId = Constants.getInt("headerFooterGroupId");
			int globalMenuGroupId = Constants.getInt("menuGroupId");
			if(header!=null) remoteTemp.setHeaderDocId(createLocalDoc(header, globalHeaderFooterGroupId));
			if(footer!=null) remoteTemp.setFooterDocId(createLocalDoc(footer, globalHeaderFooterGroupId));
			if(menu != null) remoteTemp.setMenuDocId(createLocalDoc(menu, globalMenuGroupId));

			if ("cloud".equals(Constants.getInstallName()))
			{
				String lng = getLocalLng();
				if (Tools.isNotEmpty(lng))
				{
					remoteTemp.setTempName(oTempName + "-"+lng);
					remoteTemp.setLng(lng);
				}
			}

			remoteTemp.setTempId(-1);

			//replace available groups for local groups
			remoteTemp.setAvailableGroups(convertGroupIdsToLocalFilter(remoteTemp.getAvailableGroups()));

			TemplatesDB.getInstance().saveTemplate(remoteTemp);
			localTempId = remoteTemp.getTempId();

			remoteTemp.setTempId(oTempId);
			remoteTemp.setTempName(oTempName);
			remoteTemp.setLng(oTempLng);
			remoteTemp.setHeaderDocId(oTempHeaderDocId);
			remoteTemp.setFooterDocId(oTempFooterDocId);
			remoteTemp.setMenuDocId(oTempMenuDocId);
			remoteTemp.setAvailableGroups(oAvailableGroups);
		}
		if (localTempId < 1)
		{
			context.getRequest().setAttribute("errorText", "Neexistuje lokálna šablóna "+remoteTemp.getTempName());
			Logger.debug(SyncDirAction.class, "Neexistuje local sablona "+remoteTemp.getTempName());
			return -1;
		}
		return localTempId;
	}

	/**
	 * Vytvori local doc s tym ze neberie ohlad na to v akom adresari bol povodne
	 * ale vytvori ho v adresari s groupid z parametra (vytvori ho len ak tam taky doc este nie je, zistuje sa podla title)
	 * @param doc
	 * @param forceLocalGroupId
	 * @author mhalas
	 * @return
	 */
	private int createLocalDoc(DocDetails doc, int forceLocalGroupId)
	{
		int docid = new SimpleQuery().forInt("Select doc_id from documents where group_id = ? and title = ?", forceLocalGroupId,doc.getTitle());
		if(docid <= 0)
		{
			int oGroupId = doc.getGroupId();
			doc.setGroupId(forceLocalGroupId);
			docid = createLocalDoc(doc);
			doc.setGroupId(oGroupId);
		}
		return docid;
	}

	private Content getRemoteContent()
	{
		if (null == remoteContent)
		{
			remoteContent = (Content) getLocalFile("content.xml");
		}
		return remoteContent;
	}

	/**
	 * Vrati zoznam importovanych suborov, vo formate vhodnom na vypisanie.
	 * Vacsina funkcie sa tyka nastavenia spravneho statusu (neexistuje, je starsi, je novsi, je iny, je rovnaky).
	 *
	 * @return
	 */
	public List<ContentFileBean> getRemoteContentFiles()
	{
		List<ContentFileBean> fileBeans = new ArrayList<>();
		Content content = getRemoteContent();
		if (null == content) return fileBeans;

		for (Numbered<Content.File> numFile : Numbered.list(content.getFiles()))
		{
			Content.File file = numFile.item;
			String path = file.getVirtualPath();
			int status = ContentFileBean.STATUS_MISSING;
			String localPath = Tools.getRealPath(path);
			IwcmFile localFile = new IwcmFile(localPath);
			if (localFile.exists())
			{
				long remoteTime = file.getTime();
				long localTime = localFile.lastModified();
				if (remoteTime > localTime)
				{
					status = ContentFileBean.STATUS_NEWER;
				}
				else if (remoteTime < localTime)
				{
					status = ContentFileBean.STATUS_OLDER;
				}
				else
				{
					status = (localFile.length() == file.getSize()) ? ContentFileBean.STATUS_CURRENT_SAME : ContentFileBean.STATUS_CURRENT_DIFFERENT;
				}
			}
			ContentFileBean fileBean = new ContentFileBean(numFile.number, path, status);
			fileBeans.add(fileBean);
		}
		return fileBeans;
	}

	/**
	 * Vrati zoznam importovanych bannerov, vo formate vhodnom na vypisanie.
	 *
	 * @return
	 */
	public List<ContentBannerBean> getRemoteContentBanners()
	{
		return BannerImporter.getBanners(getRemoteContent());
	}

	/**
	 * Vrati zoznam importovanych ankiet, vo formate vhodnom na vypisanie.
	 *
	 * @return
	 */
	public List<ContentInquiryBean> getRemoteContentInquiries()
	{
		return InquiryImporter.getInquiries(getRemoteContent(), context.getRequest());
	}

	/**
	 * Vrati zoznam importovanych adresarov galerii, vo formate vhodnom na vypisanie.
	 *
	 * @return
	 */
	public List<ContentGalleryBean.Info> getRemoteContentGalleryInfos()
	{
		return GalleryImporter.getGalleryInfos(getRemoteContent());
	}

	/**
	 * Vrati zoznam importovanych obrazkov galerie, vo formate vhodnom na vypisanie.
	 *
	 * @return
	 */
	public List<ContentGalleryBean.Image> getRemoteContentGalleryImages()
	{
		return GalleryImporter.getGalleryImages(getRemoteContent(), context.getRequest());
	}

	/**
	 * Vrati zoznam remote adresarov
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public List<GroupDetails> getRemoteGroups()
	{
		if (remoteGroups==null && remoteGroupId>0)
		{
			remoteGroups = (List<GroupDetails>) getLocalFile("groups.xml");
		}

		return remoteGroups;
	}

	/**
	 * Vrati remote adresar na zaklade jeho ID
	 * @param remoteGroupId
	 * @return
	 */
	public GroupDetails getRemoteGroup(int remoteGroupId)
	{
		for (GroupDetails remoteGroup : getRemoteGroups())
		{
			if (remoteGroup.getGroupId()==remoteGroupId)
			{
				return remoteGroup;
			}
		}
		return null;
	}

	/**
	 * Vrati remote web stranky
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public List<DocDetails> getRemoteDocs()
	{
		if (remoteDocs==null && remoteGroupId>0)
		{
			remoteDocs = (List<DocDetails>) getLocalFile("docs.xml");
		}
		/**
		 * musime to zosortovat tak aby prve boli hlavne stranky priecinkov
		 * inak mozu nastat problemy pri generovani virtualPath pri cloud prekladoch
		 * ticket 15053
		 */
		sortByMainPages();
		return remoteDocs;

	}

	/**
	 * Zosortuje zoznam doc-s podla toho ci su hlavne stranky priecinkov
	 * ak je stranka hlavna nejakemu priecinku tak bude niekde na zaciatku zoznamu
	 *
	 * @author mhalas
	 */
	private void sortByMainPages()
	{
		if (remoteDocs == null || remoteGroups == null) return;

		Set<Integer> mainPages = new HashSet<>();
		for(GroupDetails gd : remoteGroups)
		{
			mainPages.add(gd.getDefaultDocId());
		}
		//Logger.debug(getClass(), "main pages docids : " + Arrays.toString(mainPages.toArray()));
		final Set<Integer> finalMainPages = mainPages;

		//Logger.debug(getClass(), "docids before sort: " + Arrays.toString(Lambda.convert(remoteDocs, new PropertyExtractor("docId")).toArray()));

		Collections.sort(remoteDocs, new Comparator<DocDetails>(){
			@Override
			public int compare(DocDetails o1, DocDetails o2)
			{
				if(finalMainPages.contains(o1.getDocId()) && finalMainPages.contains(o2.getDocId())) return 0;
				if(finalMainPages.contains(o1.getDocId()) && !finalMainPages.contains(o2.getDocId())) return -1;
				if(!finalMainPages.contains(o1.getDocId()) && finalMainPages.contains(o2.getDocId())) return 1;
				return o1.getSortPriority() - o2.getSortPriority();
			}
		});

		//Logger.debug(getClass(), "docids after sort: " + Arrays.toString(Lambda.convert(remoteDocs, new PropertyExtractor("docId")).toArray()));
	}

	@SuppressWarnings("unchecked")
	public List<Media> getRemoteMedia()
	{
		if (remoteMedia==null)
		{
			remoteMedia = (List<Media>) getLocalFile("media.xml");
		}
		return remoteMedia;
	}

	/**
	 * Ziska root path z remote adresara (spolocna pre vsetky remote podadresare)
	 * @return
	 */
	public String getRemoteRootPath()
	{
		if (remoteRootPath == null)
		{
			if (getRemoteGroups().size()>0)
			{
				remoteRootPath = getRemoteGroups().get(0).getFullPath();
				//int i = remoteRootPath.lastIndexOf('/');
				//if (i>0) remoteRootPath = remoteRootPath.substring(0, i);
			}
		}
		return remoteRootPath;
	}

	/**
	 * Ziska local cestu k adresaru
	 * @param remoteGroup
	 * @return
	 */
	public String getLocalPath(GroupDetails remoteGroup)
	{
		//Logger.debug(SyncDirAction.class, "getLocalPath, rgfp="+remoteGroup.getFullPath()+" root="+getRemoteRootPath());

		String basePath = remoteGroup.getFullPath().substring(getRemoteRootPath().length());
		if("cloud".equals(Constants.getInstallName()))
		{
			String lng = getLocalLng();
			if (Tools.isNotEmpty(lng) && basePath.length()>1 && basePath.startsWith("/System")==false)
			{
				String[] exploded = Tools.getTokens(basePath, "/");
				StringBuilder translated = new StringBuilder();
				for (String str : exploded)
				{
					//basePath = CloudToolsForCore.translate(lng, basePath);
					translated.append('/');
					translated.append(CloudToolsForCore.translate(lng, str));
				}
				basePath = translated.toString();
				Logger.debug(getClass(), "translating basePath to: " + basePath);
			}
		}
		String localPath = getLocalRootGroup().getFullPath()+basePath; //TODO: preklad?
		return localPath;
	}

	/**
	 * Ziska local adresar
	 * @param remoteGroupId
	 * @return
	 */
	public GroupDetails getLocalGroup(int remoteGroupId)
	{
		GroupDetails remoteGroup = getRemoteGroup(remoteGroupId);
		if (remoteGroup!=null) return getLocalGroup(remoteGroup);
		return null;
	}

	/**
	 * Ziska local adresar
	 * @param remoteGroup
	 * @return
	 */
	public GroupDetails getLocalGroup(GroupDetails remoteGroup)
	{
		String localPath = getLocalPath(remoteGroup);
		try
		{
			return GroupsDB.getInstance().getGroupByPath(localPath);
		}
		catch (ConcurrentModificationException ex)
		{
			GroupsDB groupsDB = GroupsDB.getInstance(true);
			return groupsDB.getGroupByPath(localPath);
		}
	}

	/**
	 * Ziska local adresar
	 * @param localPath
	 * @return
	 */
	public GroupDetails getLocalGroup(String localPath)
	{
		return GroupsDB.getInstance().getGroupByPathAndDomain(localPath, localSrcGroup.getDomainName());
	}

	/**
	 * Ziska local root adresar (na zaklade zadaneho localGroupId)
	 * @return
	 */
	public GroupDetails getLocalRootGroup()
	{
		if (localRootGroup == null)
		{
			localRootGroup = GroupsDB.getInstance().getGroup(getLocalGroupId());
		}
		return localRootGroup;
	}

	/**
	 * Vrati local web stranku pre zadanu remote stranku
	 * @param remoteDoc
	 * @return
	 */
	public DocDetails getLocalDoc(DocDetails remoteDoc)
	{
		GroupDetails localGroup = getLocalGroup(remoteDoc.getGroupId());
		if (localGroup!=null)
		{
			//najdi local doc
			DocDB docDB = DocDB.getInstance();
			List<DocDetails> localDocs = docDB.getBasicDocDetailsByGroup(localGroup.getGroupId(), DocDB.ORDER_PRIORITY);
			for (DocDetails doc : localDocs)
			{
				if (comapreDocs(doc, remoteDoc))
				{
					return(doc);
				}
			}
		}
		return null;
	}

	private boolean comapreDocs(DocDetails doc, DocDetails remoteDoc) {
		if("url".equals(this.compareBy)) {
			return (Tools.isNotEmpty(remoteDoc.getVirtualPath()) && Tools.isNotEmpty(doc.getVirtualPath()) && doc.getVirtualPath().equals(remoteDoc.getVirtualPath()));
		} else if("none".equals(this.compareBy)) {
			// allways return false - aka allways create new doc
			return false;
		} else if("fieldA".equals(this.compareBy)) {
			return doc.getFieldA().equals(remoteDoc.getFieldA());
		} else if("fieldB".equals(this.compareBy)) {
			return doc.getFieldB().equals(remoteDoc.getFieldB());
		} else if("fieldC".equals(this.compareBy)) {
			return doc.getFieldC().equals(remoteDoc.getFieldC());
		} else {
			// nameOrUrl / null / or anything else - default
			return (doc.getTitle().equals(remoteDoc.getTitle()) || (Tools.isNotEmpty(remoteDoc.getVirtualPath()) && Tools.isNotEmpty(doc.getVirtualPath()) && doc.getVirtualPath().equals(remoteDoc.getVirtualPath()) ));
		}
	}

	/**
	 * Vrati nazov vzdialenej sablony podla id
	 * @param remoteTempId
	 * @return
	 */
	public String getRemoteTempName(int remoteTempId)
	{
		TemplateDetails temp = getRemoteTemp(remoteTempId);
		return (null == temp) ? null : temp.getTempName();
	}

	/**
	 * Vrati vzdialenu sablonu podla id
	 * @param remoteTempId
	 * @return
	 */
	public TemplateDetails getRemoteTemp(int remoteTempId)
	{
		for (TemplateDetails temp : getRemoteTemps())
		{
			if (temp.getTempId()==remoteTempId) return temp;
		}
		return null;
	}

	/**
	 * Vrati nazov lokalnej sablony podla id
	 * @param localTempId
	 * @return
	 */
	public String getLocalTempName(int localTempId)
	{
		TemplateDetails temp = TemplatesDB.getInstance().getTemplate(localTempId);
		if (temp!=null) return temp.getTempName();
		return null;
	}

	/**
	 * Vrati id lokalnej sablony podla nazvu
	 * @param tempName
	 * @return
	 */
	public int getLocalTempId(String tempName)
	{
		if ("cloud".equals(Constants.getInstallName()))
		{
			String lng = getLocalLng();
			if (Tools.isNotEmpty(lng))
			{
				tempName = tempName + "-"+lng;
			}
		}

		TemplateDetails temp = TemplatesDB.getInstance().getTemplate(tempName);
		if (temp!=null) return temp.getTempId();
		return -1;
	}

	/**
	 * Vrati zoznam remote sablon
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public List<TemplateDetails> getRemoteTemps()
	{
		if (remoteTemps == null)
		{
			remoteTemps = (List<TemplateDetails>) getLocalFile("temps.xml");
		}
		return remoteTemps;
	}

	private int getCreateUserGroup(int remoteUserGroupId)
	{
		UserGroupDetails remoteUserGroup = getRemoteUserGroup(remoteUserGroupId);
		if (remoteUserGroup==null)
		{
			context.getRequest().setAttribute("errorText", "Neexistuje remote pouzivatelska skupina pre userGroupId="+remoteUserGroupId);
			Logger.debug(SyncDirAction.class, "Neexistuje remote pouzivatelska skupina pre userGroupId="+remoteUserGroupId);
			return -1;
		}
		int localUserGroupId = getLocalUserGroupId(remoteUserGroup.getUserGroupName());
		if (localUserGroupId < 1 && createMissingUserGroups)
		{
			int oUserGroupId = remoteUserGroup.getUserGroupId();

			remoteUserGroup.setUserGroupId(-1);
			UserGroupsDB.saveUserGroup(remoteUserGroup);
			localUserGroupId = remoteUserGroup.getUserGroupId();

			remoteUserGroup.setUserGroupId(oUserGroupId);
		}
		if (localUserGroupId < 1)
		{
			context.getRequest().setAttribute("errorText", "Neexistuje lokálna používateľská skupina "+remoteUserGroup.getUserGroupName());
			Logger.debug(SyncDirAction.class, "Neexistuje lokálna používateľská skupina "+remoteUserGroup.getUserGroupName());
			return -1;
		}
		return localUserGroupId;
	}

	/**
	 * Vrati remote nazov user group podla zadaneho id
	 * @param remoteUserGroupId
	 * @return
	 */
	public String getRemoteUserGroupName(int remoteUserGroupId)
	{
		UserGroupDetails ugd = getRemoteUserGroup(remoteUserGroupId);
		return (null == ugd) ? null : ugd.getUserGroupName();
	}

	/**
	 * Vrati vzdialenu pouzivatelsku skupinu na zaklade jej ID
	 * @param remoteUserGroupId
	 * @return
	 */
	public UserGroupDetails getRemoteUserGroup(int remoteUserGroupId)
	{
		for (UserGroupDetails ugd : getRemoteUserGroups())
		{
			if (ugd.getUserGroupId()==remoteUserGroupId) return ugd;
		}
		return null;
	}

	/**
	 * Vrati lokalne id pouzivatelskej skupiny podla nazvu
	 * @param userGroupName
	 * @return
	 */
	public int getLocalUserGroupId(String userGroupName)
	{
		UserGroupDetails ugd = getLocalUserGroup(userGroupName);
		return (null == ugd) ? -1 : ugd.getUserGroupId();
	}

	/**
	 * Vrati lokalnu pouzivatelsku skupinu na zaklade jej nazvu
	 * @param userGroupName
	 * @return
	 */
	public UserGroupDetails getLocalUserGroup(String userGroupName)
	{
		return UserGroupsDB.getInstance().getUserGroup(userGroupName);
	}

	/**
	 * Vrati lokalnu pouzivatelsku skupinu na zaklade jej ID
	 * @param localUserGroupId
	 * @return
	 */
	public UserGroupDetails getLocalUserGroup(int localUserGroupId)
	{
		return UserGroupsDB.getInstance().getUserGroup(localUserGroupId);
	}

	/**
	 * Vrati zoznam remote user groups
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public List<UserGroupDetails> getRemoteUserGroups()
	{
		if (remoteUserGroups == null)
		{
			remoteUserGroups = (List<UserGroupDetails>) getLocalFile("usergroups.xml");
		}
		return remoteUserGroups;
	}


	private int getCreatePerexGroup(int remotePerexGroupId)
	{
		PerexGroupBean remotePerexGroup = getRemotePerexGroup(remotePerexGroupId);
		if (remotePerexGroup==null)
		{
			context.getRequest().setAttribute("errorText", "Neexistuje remote perex skupina pre perexGroupId="+remotePerexGroupId);
			Logger.debug(SyncDirAction.class, "Neexistuje remote perex skupina pre perexGroupId="+remotePerexGroupId);
			return -1;
		}
		int localPerexGroupId = getLocalPerexGroupId(remotePerexGroup.getPerexGroupName());
		if (localPerexGroupId < 1 && createMissingUserGroups)
		{
			//premigrovat ID adresarov, POZOR: v groups.xml mame len exportovane skupiny, takze to nevie setnut prava na ine skupiny ako exportovane
			StringBuilder availableGroups = new StringBuilder("");
			if (Tools.isNotEmpty(remotePerexGroup.getAvailableGroups())) {
				int[] remoteGroupIds = Tools.getTokensInt(remotePerexGroup.getAvailableGroups(), ",");
				for (int groupId : remoteGroupIds) {
					GroupDetails group = getLocalGroup(groupId);
					if (group != null) {
						if (Tools.isNotEmpty(availableGroups)) availableGroups.append(",");
						availableGroups.append(String.valueOf(group.getGroupId()));
					}
				}
			}
			DocDB.getInstance().savePerexGroup(remotePerexGroup.getPerexGroupName(), -1, availableGroups.toString(), null);
			localPerexGroupId = getLocalPerexGroupId(remotePerexGroup.getPerexGroupName());
		}
		if (localPerexGroupId < 1)
		{
			context.getRequest().setAttribute("errorText", "Neexistuje lokálna perex skupina "+remotePerexGroup.getPerexGroupName());
			Logger.debug(SyncDirAction.class, "Neexistuje lokálna perex skupina "+remotePerexGroup.getPerexGroupName());
			return -1;
		}
		return localPerexGroupId;
	}

	/**
	 * Vrati remote nazov perex group podla zadaneho id
	 * @param remotePerexGroupId
	 * @return
	 */
	public String getRemotePerexGroupName(int remotePerexGroupId)
	{
		PerexGroupBean pgb = getRemotePerexGroup(remotePerexGroupId);
		return (null == pgb) ? null : pgb.getPerexGroupName();
	}

	/**
	 * Vrati vzdialenu perex skupinu na zaklade jej ID
	 * @param remotePerexGroupId
	 * @return
	 */
	public PerexGroupBean getRemotePerexGroup(int remotePerexGroupId)
	{
		for (PerexGroupBean pgd : getRemotePerexGroups())
		{
			if (pgd.getPerexGroupId()==remotePerexGroupId) return pgd;
		}
		return null;
	}

	/**
	 * Vrati lokalne id perex skupiny podla nazvu
	 * @param perexGroupName
	 * @return
	 */
	public int getLocalPerexGroupId(String perexGroupName)
	{
		PerexGroupBean pgd = getLocalPerexGroup(perexGroupName);
		return (null == pgd) ? -1 : pgd.getPerexGroupId();
	}

	/**
	 * Vrati lokalnu perex skupinu na zaklade jej nazvu
	 * @param perexGroupName
	 * @return
	 */
	public PerexGroupBean getLocalPerexGroup(String perexGroupName)
	{
		return DocDB.getInstance().getPerexGroupByName(perexGroupName);
	}

	/**
	 * Vrati lokalnu perex skupinu na zaklade jej ID
	 * @param localPerexGroupId
	 * @return
	 */
	public PerexGroupBean getLocalPerexGroup(int localPerexGroupId)
	{
		return DocDB.getInstance().getPerexGroup(localPerexGroupId, null);
	}


	/**
	 * Vrati zoznam remote perex groups
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public List<PerexGroupBean> getRemotePerexGroups()
	{
		if (remotePerexGroups == null)
		{
			remotePerexGroups = (List<PerexGroupBean>) getLocalFile("perexgroups.xml");
		}
		return remotePerexGroups;
	}

	@Override
	public ActionBeanContext getContext()
	{
		return context;
	}
	@Override
	public void setContext(ActionBeanContext context)
	{
		super.setContext(context);

		Identity user = getUser();
		if (user != null)
		{
			// ak je true synchrozujeme s xml subormi v syncdir a nastavime fake loginy, ktore sa ale nikde nepouzije, su tam len kvoli valid a pod.
			if (Tools.isNotEmpty( context.getRequest().getParameter("syncDir")))
			{
				setSyncDir(context.getRequest().getParameter("syncDir"));
			}
		}

		//Set comapreBy
		this.compareBy = context.getRequest().getParameter("compareBy");
		if(this.compareBy == null) this.compareBy = "nameOrUrl";
	}

	public void setSyncDir(String syncDir)
	{
		this.syncDir = syncDir;
	}

	public int getLocalGroupId()
	{
		return localGroupId;
	}

	public void setLocalGroupId(int localGroupId)
	{
		this.localGroupId = localGroupId;

		this.localSrcGroup = GroupsDB.getInstance().getGroup(localGroupId);
	}

	public GroupDetails getLocalSrcGroup()
	{
		return localSrcGroup;
	}

	public int getRemoteGroupId()
	{
		return remoteGroupId;
	}

	public void setRemoteGroupId(int remoteGroupId)
	{
		this.remoteGroupId = remoteGroupId;
	}

	public boolean isCreateMissingTemplates()
	{
		return createMissingTemplates;
	}

	public void setCreateMissingTemplates(boolean createMissingTemplates)
	{
		this.createMissingTemplates = createMissingTemplates;
	}

	public boolean isCreateMissingUserGroups()
	{
		return createMissingUserGroups;
	}

	public void setCreateMissingUserGroups(boolean createMissingUserGroups)
	{
		this.createMissingUserGroups = createMissingUserGroups;
	}

	/**
	 * Vrati dekodovany objekt z XML suboru.
	 *
	 * @param filename  retazec obsahujuci nazov XML suboru
	 * @return          objekt z tohto suboru, alebo null ak nieco nevyjde
	 */
	private Object getLocalFile(String filename)
	{
		InputStream inputStream = null;
		InputStream inputStream2 = null;
		try
		{
			String realPath = Tools.getRealPath(syncDir) + java.io.File.separatorChar + filename;
			Logger.debug(SyncDirAction.class, "Reading local file: "+realPath);
			inputStream = new IwcmInputStream(realPath);
			inputStream2 = checkXmlForAttack(inputStream);
			XMLDecoder decoder = new XMLDecoder(inputStream2, null, new WarningListener());
			Object o = decoder.readObject();
			decoder.close();
			//Logger.debug(SyncDirAction.class, "readed, o="+o);
			return o;
		}
		catch (IOException|IndexOutOfBoundsException ex)
		{
			sk.iway.iwcm.Logger.error(ex);
			errorMessages.add(ex.getMessage());
		}
		finally {
			if (inputStream != null) {
				try { inputStream.close(); } catch (Exception ex) {}
			}
			if (inputStream2 != null) {
				try { inputStream2.close(); } catch (Exception ex) {}
			}
		}
		return null;
	}

	public void cleanFiles()
	{
		if (!Tools.isEmpty(syncDir))
		{
			Content content = getRemoteContent();
			if (content!=null && content.getFiles()!=null) {
				String tmpDir = Tools.getRealPath(syncDir);
				for (Content.File file : content.getFiles())
				{
					new IwcmFile(tmpDir + java.io.File.separatorChar + "/content/" + file.getZipPath().replace("/", File.separator)).delete();
				}
				new IwcmFile(tmpDir + java.io.File.separatorChar + "/content").delete();
				for (String filename : new String[]{ "docs.xml", "groups.xml", "temps.xml", "perexgroups.xml", "usergroups.xml", "content.xml" })
				{
					new IwcmFile(tmpDir + java.io.File.separatorChar + filename).delete();
				}
				new IwcmFile(tmpDir).delete();
			}
		}
	}

	private Identity getUser()
	{
		return (Identity) context.getRequest().getSession().getAttribute(Constants.USER_KEY);
	}

	public String getLocalDomain()
	{
		return localDomain;
	}

	public void setLocalDomain(String localDomain)
	{
		this.localDomain = localDomain;
	}

	/**
	 * Jednoduche premapovanie ID adresarov na nove hodnoty v lokalnom systeme
	 * @return
	 */
	private String fixGroupIdsInContent(String htmlCode)
	{
		int start = htmlCode.indexOf("groupIds=");
		if (start==-1) return htmlCode;

		//prehladame hodnoty groupIds= a nahradime ich za novy vyraz
		Matcher matcher = Pattern.compile("!INCLUDE\\(/components/[^)]*groupIds=(['\"&quot;+1234567890]*)[^)]*\\)!").matcher(htmlCode);
		while (matcher.find())
		{
		 	Logger.debug(SyncDirAction.class, "Mam:");
		 	Logger.debug(SyncDirAction.class, matcher.group());
		 	Logger.debug(SyncDirAction.class, matcher.group(1));
			String groupIds = matcher.group(1);
			Logger.debug(SyncDirAction.class, "groupIds="+groupIds);
			String groupIdsOrig = groupIds;
			groupIds = Tools.replace(groupIds, "&quot;", "\"");
			groupIds = groupIds.replace('"', ' ');
			groupIds = groupIds.replace('\'', ' ');
			groupIds = groupIds.trim();
			int[] groupIdsArr = Tools.getTokensInt(groupIds, "+");
			StringBuilder newGroupIds = new StringBuilder();
			for (int groupId : groupIdsArr)
			{
				int localId = groupId;
				GroupDetails localGroup = getLocalGroup(groupId);
				if (localGroup != null) localId = localGroup.getGroupId();
				if (newGroupIds.length()>0) newGroupIds.append('+');
				newGroupIds.append(localId);
			}

			if (Tools.isEmpty(groupIdsOrig) || Tools.isEmpty(newGroupIds.toString())) continue;

			Logger.debug(SyncDirAction.class, "replacing to:"+newGroupIds.toString());
			Logger.debug(SyncDirAction.class, "test:"+"groupIds="+groupIdsOrig+",");

			if (groupIdsOrig.charAt(0)=='"' || groupIdsOrig.charAt(0)=='\'')
			{
				newGroupIds.insert(0, groupIdsOrig.charAt(0));
				newGroupIds.append(groupIdsOrig.charAt(0));
			}

			htmlCode = Tools.replace(htmlCode, "groupIds="+groupIdsOrig+",", "groupIds="+newGroupIds.toString()+",");
			htmlCode = Tools.replace(htmlCode, "groupIds="+groupIdsOrig+")", "groupIds="+newGroupIds.toString()+")");
			htmlCode = Tools.replace(htmlCode, "groupIds="+groupIdsOrig+" ", "groupIds="+newGroupIds.toString()+" ");
		}

		return htmlCode;
	}

	private String fixMediaGroupIdsInContent(String htmlCode)
	{
		int start = htmlCode.indexOf("/components/media/media");
		if (start==-1) return htmlCode;

		//prehladame hodnoty groupIds= a nahradime ich za novy vyraz
		Matcher matcher = Pattern.compile("!INCLUDE\\(/components/media/media[^)]*groups=(['\"&quot;+1234567890]*)[^)]*\\)!").matcher(htmlCode);
		while (matcher.find())
		{
		 	Logger.debug(SyncDirAction.class, "Mam:");
		 	Logger.debug(SyncDirAction.class, matcher.group());
		 	Logger.debug(SyncDirAction.class, matcher.group(1));
			String mediaGroupIds = matcher.group(1);
			Logger.debug(SyncDirAction.class, "mediaGroupIds="+mediaGroupIds);
			String groupIdsOrig = mediaGroupIds;
			mediaGroupIds = Tools.replace(mediaGroupIds, "&quot;", "\"");
			mediaGroupIds = mediaGroupIds.replace('"', ' ');
			mediaGroupIds = mediaGroupIds.replace('\'', ' ');
			mediaGroupIds = mediaGroupIds.trim();
			int[] mediaGroupIdsArr = Tools.getTokensInt(mediaGroupIds, "+");
			StringBuilder newMediaGroupIds = new StringBuilder();
			for (int remoteMediaGroupId : mediaGroupIdsArr)
			{
				Integer localMediaGroupId = remoteToLocalMediaGroups.get(remoteMediaGroupId);
				if (localMediaGroupId == null) localMediaGroupId = remoteMediaGroupId;

				if (newMediaGroupIds.length()>0) newMediaGroupIds.append('+');
				newMediaGroupIds.append(localMediaGroupId);
			}

			if (Tools.isEmpty(groupIdsOrig) || Tools.isEmpty(newMediaGroupIds.toString())) continue;

			Logger.debug(SyncDirAction.class, "replacing mediaGroup to:"+newMediaGroupIds.toString()+" original="+groupIdsOrig+";");

			if (groupIdsOrig.charAt(0)=='"' || groupIdsOrig.charAt(0)=='\'')
			{
				newMediaGroupIds.insert(0, groupIdsOrig.charAt(0));
				newMediaGroupIds.append(groupIdsOrig.charAt(0));
			}

			htmlCode = Tools.replace(htmlCode, "groups="+groupIdsOrig+",", "groups="+newMediaGroupIds.toString()+",");
			htmlCode = Tools.replace(htmlCode, "groups="+groupIdsOrig+")", "groups="+newMediaGroupIds.toString()+")");
			htmlCode = Tools.replace(htmlCode, "groups="+groupIdsOrig+" ", "groups="+newMediaGroupIds.toString()+" ");
		}

		return htmlCode;
	}

	/**
	 * Vrati jazyk do ktoreho sa ma vykonat preklad pri klonovani
	 * @return
	 */
	private String getLocalLng()
	{
		String lng = getRequest().getParameter("language");

		if ("cloud".equals(Constants.getInstallName()))
		{
			if ("sk".equals(lng) || "cz".equals(lng) || "de".equals(lng))
			{
				return lng;
			}
			return null;
		}

		return lng;
	}

	private void saveRollbackJson(List<Integer> historyIds, List<Integer> historyGroupsIds)
	{
		String archivePath = "/files/protected/backup/"; //NOSONAR
		String archiveName = localGroupId + "_" + new SimpleDateFormat("dd-MM-yyyy_HH-mm-ss").format(new Date())+".json";
		BufferedOutputStream output = null;
		try
		{
			JSONArray docs = new JSONArray();
			for(Integer id : historyIds)
			{
				JSONObject item = new JSONObject();
				item.put("historyId",  id);
			   docs.put(item);
			}

			JSONArray groups = new JSONArray();
			for(Integer id : historyGroupsIds)
			{
				JSONObject item = new JSONObject();
				item.put("historyGroupId",  id);
				groups.put(item);
			}

			JSONObject result = new JSONObject();
			result.put("docs", docs);
			result.put("groups", groups);

			IwcmFile dir = IwcmFile.fromVirtualPath(archivePath);
			Logger.debug(SyncDirAction.class, "Archive directory: "+dir.getAbsolutePath()+" exists: "+dir.exists());
			if(!dir.exists())
			{
				Logger.debug(SyncDirAction.class, "Creating directory: "+dir.getAbsolutePath());
				dir.mkdirs();
			}

			output = new BufferedOutputStream(new IwcmOutputStream(new IwcmFile(PathFilter.getRealPath(archivePath), archiveName)));
			output.write(result.toString(3).getBytes(StandardCharsets.UTF_8));
			output.close();
			output = null;
		}
		catch(Exception e)	{sk.iway.iwcm.Logger.error(e);}
		finally
		{
			if(output!=null)
			{
				try						{output.close();}
				catch (Exception ex)	{sk.iway.iwcm.Logger.error(ex);}
			}
		}
	}

	private List<Integer> getNewGroups(Date start)
	{
		List<Integer> result = new ArrayList<>();

		Connection db_conn = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		try
		{
			db_conn = DBPool.getConnection();
			ps = db_conn.prepareStatement("SELECT schedule_id FROM groups_scheduler WHERE save_date >= ?");
			ps.setTimestamp(1, new Timestamp(start.getTime()));
			rs = ps.executeQuery();
			while (rs.next())
			{
				result.add(rs.getInt("schedule_id"));
			}
			rs.close();
			ps.close();
			db_conn.close();
			rs = null;
			ps = null;
			db_conn = null;
		}
		catch (Exception ex)
		{
			sk.iway.iwcm.Logger.error(ex);
		}
		finally
		{
			try
			{
				if (rs != null)
					rs.close();
				if (ps != null)
					ps.close();
				if (db_conn != null)
					db_conn.close();
			}
			catch (Exception ex2)
			{
			}
		}

		return result;
	}

	@SuppressWarnings("unchecked")
	public Map<Integer, DocNoteBean> getRemoteDocsNotes()
	{
		if (remoteNotesMap==null)
		{
			remoteNotesMap = new HashMap<>();
			List<DocNoteBean> remoteNotes = (List<DocNoteBean>) getLocalFile("notes.xml");

			if (remoteNotes != null)
			{
				for (DocNoteBean note : remoteNotes)
				{
					remoteNotesMap.put(note.getDocId(), note);
				}
			}
		}

		return remoteNotesMap;
	}

	// [#37105 - Druhy nalez z penetracnycch testov] - uloha #0
	public static InputStream checkXmlForAttack(InputStream inputStream) throws IOException {
		String xmlDecoderAllowedClasses = Constants.getString("XMLDecoderAllowedClasses", "");
		List<String> allowedClasses = new ArrayList<>();
		for (String className : Tools.getTokens(xmlDecoderAllowedClasses, ",")) {
			className = className.trim();
			//toto nemozeme povolit, nie je na to dovod
			if (className.contains("java.lang.Runtime")) continue;

			allowedClasses.add(className);
		}

		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		org.apache.commons.io.IOUtils.copy(inputStream, baos);

		try { inputStream.close(); } catch (IOException ex) { sk.iway.iwcm.Logger.error(ex); }

		byte[] bytes = baos.toByteArray();

		final String xml = new String(bytes, StandardCharsets.UTF_8);
		final String regex = "class=\"[\\w\\.]*\"";
		final Pattern pattern = Pattern.compile(regex, Pattern.MULTILINE);
		final Matcher matcher = pattern.matcher(xml);

		final Set<String> classes = new HashSet<>();
		while (matcher.find()) {
			String match = matcher.group(0);
			match = Tools.replace(match, "\"", "");
			match = Tools.replace(match, "class=", "");
			//musi to obsahovat nejaky package, v autoupdate v TB sme mali HTML kod sablon a tam bolo class="meno" vramci HTML kodu
			if (match.contains(".")) classes.add(match);
		}

		if (!allowedClasses.containsAll(classes)) {
			List<String> notAllowedClasses = classes.stream().filter(c -> !allowedClasses.contains(c)).collect(Collectors.toList());
			String message = String.format("XMLDecoder - not allowed classes found: %s", Tools.join(notAllowedClasses, ", "));
			log.debug(message);
			throw new IOException(message);
		}

		return new ByteArrayInputStream(bytes);
	}

	/**
	 * Prepare map of MEDIA groups and create local MediaGroups if not exists
	 */
	private Map<Integer, Integer> prepareRemoteToLocalMediaGroupMap() {
		Map<Integer, Integer> remoteToLocalMediaGroupMap = new HashMap<>();

		List<Media> media = getRemoteMedia();
		if (media != null)
		{
			for (Media m : media)
			{
				for (MediaGroupBean remoteGroup : m.getGroups()) {
					if (remoteToLocalMediaGroupMap.get(remoteGroup.getMediaGroupId()) != null) {
						continue;
					}

					MediaGroupBean localMediaGroup = MediaDB.getGroup(remoteGroup.getMediaGroupName());
					if (localMediaGroup == null) {
						localMediaGroup = new MediaGroupBean();
						localMediaGroup.setMediaGroupName(remoteGroup.getMediaGroupName());

						//try to map availableGroups
						if (Tools.isNotEmpty(remoteGroup.getAvailableGroups())) {
							int[] remoteGroupIds = Tools.getTokensInt(remoteGroup.getAvailableGroups(), ",");
							StringBuilder availableGroups = new StringBuilder();
							for (int remoteGroupId : remoteGroupIds) {
								GroupDetails localGroup = getLocalGroup(remoteGroupId);
								if (localGroup != null) {
									if (availableGroups.length() > 0) availableGroups.append(",");
									availableGroups.append(String.valueOf(localGroup.getGroupId()));
								}
							}
							localMediaGroup.setAvailableGroups(availableGroups.toString());
						}

						localMediaGroup.save();
					}
					remoteToLocalMediaGroupMap.put(remoteGroup.getMediaGroupId(), localMediaGroup.getMediaGroupId());
				}
			}
		}

		return remoteToLocalMediaGroupMap;
	}
}