/**********************************************************************
* This file is part of iDempiere ERP Open Source *
* http://www.idempiere.org *
* *
* Copyright (C) Contributors *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License *
* as published by the Free Software Foundation; either version 2 *
* 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 General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, *
* MA 02110-1301, USA. *
* *
* Contributors: *
* - Trek Global Corporation *
* - Heng Sin Low *
**********************************************************************/
package com.trekglobal.idempiere.rest.api.util;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.ws.rs.container.ContainerRequestContext;
import org.compiere.dbPort.Convert;
import org.compiere.model.I_AD_UserPreference;
import org.compiere.util.Env;
import org.compiere.util.Ini;
import org.compiere.util.Util;
/**
* Helper for the System administrator only migration script request headers.
* The headers are only honored for client 0 (System), role 0 (System Administrator)
* and a user that is an administrator.
*
* @author hengsin
*/
public final class MigrationScriptContext {
public static final String LOG_MIGRATION_SCRIPT_HEADER = "X-ID-LogMigrationScript";
public static final String ADEMPIERE_SYS_HEADER = "X-ID-AdempiereSys";
public static final String MIGRATION_SCRIPT_COMMENT_HEADER = "X-ID-MigrationScriptComment";
public static final String MIGRATION_SCRIPT_FILE_NAME = Convert.CONTEXT_MIGRATION_SCRIPT_FILE_NAME;
private static final Map s_sessionFileNameCache = new ConcurrentHashMap<>();
private static final String[] CONTEXT_KEYS = new String[] {
Ini.P_LOGMIGRATIONSCRIPT,
"P|" + Ini.P_LOGMIGRATIONSCRIPT,
Ini.P_ADEMPIERESYS,
"P|" + Ini.P_ADEMPIERESYS,
I_AD_UserPreference.COLUMNNAME_MigrationScriptComment,
"P|" + I_AD_UserPreference.COLUMNNAME_MigrationScriptComment,
MIGRATION_SCRIPT_FILE_NAME,
"P|" + MIGRATION_SCRIPT_FILE_NAME
};
private static final String MIGRATION_SCRIPT_APPLIED = MigrationScriptContext.class.getName() + ".applied";
private MigrationScriptContext() {
}
/**
* Apply the migration script headers of the request to the current context.
* Nothing is applied if the request has none of the headers or if the current
* userID > 102 or tenantID > 20 (Seed tenants and users).
*
* @param requestContext request context
*/
public static void apply(ContainerRequestContext requestContext) {
String logHeader = requestContext.getHeaderString(LOG_MIGRATION_SCRIPT_HEADER);
String adempiereSysHeader = requestContext.getHeaderString(ADEMPIERE_SYS_HEADER);
String commentHeader = requestContext.getHeaderString(MIGRATION_SCRIPT_COMMENT_HEADER);
if (logHeader == null && adempiereSysHeader == null && commentHeader == null)
return;
Properties ctx = Env.getCtx();
if (!isSeedUserAndTenant(ctx))
return;
String logMigrationScript = parseYesNo(logHeader);
String adempiereSys = parseYesNo(adempiereSysHeader);
String comment = sanitizeComment(commentHeader);
if (logMigrationScript != null)
setContext(ctx, Ini.P_LOGMIGRATIONSCRIPT, logMigrationScript);
if (adempiereSys != null)
setContext(ctx, Ini.P_ADEMPIERESYS, adempiereSys);
if (comment != null)
setContext(ctx, I_AD_UserPreference.COLUMNNAME_MigrationScriptComment, comment);
if ("Y".equalsIgnoreCase(ctx.getProperty(Ini.P_LOGMIGRATIONSCRIPT))) {
String currentComment = comment != null ? comment : Env.getContext(ctx, I_AD_UserPreference.COLUMNNAME_MigrationScriptComment);
int sessionId = Env.getContextAsInt(ctx, Env.AD_SESSION_ID);
String fileName = getOrCreateMigrationScriptFileName(sessionId, currentComment);
setContext(ctx, MIGRATION_SCRIPT_FILE_NAME, fileName);
}
if (logMigrationScript != null || comment != null)
requestContext.setProperty(MIGRATION_SCRIPT_APPLIED, Boolean.TRUE);
}
/**
* Resolve or create the migration script file name for the given session and comment.
*
* @param sessionId AD_Session_ID
* @param comment migration script comment
* @return migration script file name
*/
public static String getOrCreateMigrationScriptFileName(int sessionId, String comment) {
String normalizedComment = sanitizeComment(comment);
if (sessionId <= 0)
return Convert.getMigrationScriptFileName(normalizedComment);
String key = getSessionCacheKey(sessionId, normalizedComment);
return s_sessionFileNameCache.computeIfAbsent(key, k -> Convert.getMigrationScriptFileName(normalizedComment));
}
/**
* Flush and close the migration script files if migration script headers
* have been applied to the request.
*
* @param requestContext request context
*/
public static void closeMigrationScriptFiles(ContainerRequestContext requestContext) {
if (requestContext == null || !Boolean.TRUE.equals(requestContext.getProperty(MIGRATION_SCRIPT_APPLIED)))
return;
// Synchronize on Convert.class to be mutually exclusive with Convert.logMigrationScript
synchronized (Convert.class) {
Convert.closeLogMigrationScript();
}
}
/**
* Copy the migration script context values from source to target.
*
* @param source source context
* @param target target context
*/
public static void copy(Properties source, Properties target) {
if (source == null || target == null)
return;
for (String key : CONTEXT_KEYS) {
String value = source.getProperty(key);
if (value != null)
Env.setContext(target, key, value);
}
}
/**
* Clear cached migration script file names associated with a session.
*
* @param sessionId session id
*/
public static void cleanSessionCache(int sessionId) {
if (sessionId <= 0)
return;
String prefix = sessionId + "#";
s_sessionFileNameCache.keySet().removeIf(key -> key.startsWith(prefix));
}
/**
* Clear the entire session migration script file name cache.
*/
public static void clearSessionFileNameCache() {
s_sessionFileNameCache.clear();
}
private static String getSessionCacheKey(int sessionId, String comment) {
String ticket = null;
if (comment != null) {
Matcher m = Pattern.compile("(IDEMPIERE-[0-9]*)").matcher(comment);
if (m.find())
ticket = m.group(1);
}
String identifier = ticket != null ? ticket : (comment != null ? comment : "");
return sessionId + "#" + identifier;
}
private static boolean isSeedUserAndTenant(Properties ctx) {
return (Env.getAD_Client_ID(ctx) <= 20 && Env.getAD_User_ID(ctx) <= 102);
}
private static String parseYesNo(String value) {
if (Util.isEmpty(value, true))
return null;
String trimmed = value.trim();
if ("Y".equalsIgnoreCase(trimmed)
|| "TRUE".equalsIgnoreCase(trimmed)
|| "YES".equalsIgnoreCase(trimmed)
|| "1".equals(trimmed))
return "Y";
return "N";
}
private static String sanitizeComment(String comment) {
if (Util.isEmpty(comment, true))
return null;
// The comment is written as a single line comment in the generated migration script
String sanitized = comment.replaceAll("[\\r\\n]+", " ").replaceAll("\\s+", " ").trim();
return Util.isEmpty(sanitized, true) ? null : sanitized;
}
private static void setContext(Properties ctx, String key, String value) {
Env.setContext(ctx, key, value);
Env.setContext(ctx, "P|" + key, value);
}
}