<BO_HOME>/java/lib/cecore.jar
<BO_HOME>/java/lib/celib.jar
<BO_HOME>/java/lib/cesession.jar
<BO_HOME>/java/lib/ceplugins_core.jar
BO 4.3 SP4 BW 계정 Account Name 정규화 Java 코드를 작성하겠습니다.
---
## 처리 흐름 요약
```
SAPR3 계정 전체 조회
│
▼
이메일 없음? ──────────────────────────────► SKIP
│
▼
AccountName == 이메일 로컬파트? ───────────► 변경 불필요 (패스)
│
▼
이메일 중복 AND AccountName이 이미 BI+SI_ID? ► SKIP (규칙 3)
│
▼
이메일 로컬파트가 이미 다른 계정에 존재?
YES ─► BI+SI_ID 로 변경 (Fallback, 규칙 2)
NO ─► 이메일 로컬파트로 변경 (규칙 1)
```
---
## 주요 포인트
| 항목 | 설명 |
|---|---|
| **인증 필터** | `user.getAuthType()` 으로 `secSAPR3` 만 처리 |
| **중복 체크** | 모든 AccountName을 `Set<String>` (소문자 정규화)로 메모리에 로드 후 O(1) 비교 |
| **이메일 그룹핑** | `Map<email, List<IUser>>` 로 이메일 중복 여부 사전 파악 |
| **저장** | `infoStore.newInfoObjectCollection()` + `infoStore.commit()` |
| **Set 갱신** | commit 후 메모리 Set을 즉시 갱신해 이후 계정 처리에 반영 |
| **로그** | 변경/Fallback/스킵/오류 각각 구분 출력, 마지막에 요약 |
**컴파일 시 필요한 JAR** (BO 설치 경로 기준):
```
<BO_HOME>/java/lib/cecore.jar
<BO_HOME>/java/lib/celib.jar
<BO_HOME>/java/lib/cesession.jar
<BO_HOME>/java/lib/ceplugins_core.jar
```
import com.crystaldecisions.sdk.exception.SDKException;
import com.crystaldecisions.sdk.framework.CrystalEnterprise;
import com.crystaldecisions.sdk.framework.IEnterpriseSession;
import com.crystaldecisions.sdk.framework.ISessionMgr;
import com.crystaldecisions.sdk.occa.infostore.IInfoObject;
import com.crystaldecisions.sdk.occa.infostore.IInfoObjects;
import com.crystaldecisions.sdk.occa.infostore.IInfoStore;
import com.crystaldecisions.sdk.plugin.desktop.user.IUser;
import java.util.*;
import java.util.logging.*;
/**
* BO 4.3 SP4 - BW 연동 계정 Account Name 정규화
* ==============================================
* 처리 규칙:
* 1. SAPR3 인증 방식 계정 중 AccountName != 이메일 로컬파트(@앞) 인 계정 선별
* 2. 이메일 로컬파트로 Account Name 변경 시도
* - 해당 이름이 이미 존재하면 → "BI" + SI_ID 로 변경
* 3. 이메일 중복(동일 이메일 계정 복수) + AccountName이 이미 "BI"+SI_ID 이면 → 스킵
*/
public class BoBwAccountRename {
// ── 접속 정보 (환경에 맞게 수정) ──────────────────────────────────────
private static final String CMS_NAME = "your_cms_server"; // CMS 서버명 또는 IP
private static final String BO_USER = "Administrator"; // BO 관리자 계정
private static final String BO_PASSWORD = "your_password"; // BO 관리자 비밀번호
private static final String AUTH_TYPE = "secEnterprise"; // BO 인증 방식
private static final String SAPR3_AUTH = "secSAPR3"; // BW 연동 인증 유형
private static final Logger LOG = Logger.getLogger(BoBwAccountRename.class.getName());
// ── 결과 카운터 ────────────────────────────────────────────────────────
private int cntChanged = 0;
private int cntFallback = 0; // BI+SI_ID 로 변경된 건수
private int cntSkipped = 0;
private int cntError = 0;
public static void main(String[] args) {
// 로그 포맷 설정
System.setProperty("java.util.logging.SimpleFormatter.format",
"[%1$tF %1$tT] [%4$s] %5$s%n");
new BoBwAccountRename().run();
}
public void run() {
IEnterpriseSession session = null;
try {
session = login();
LOG.info("BO CMS 로그인 성공: " + CMS_NAME);
IInfoStore infoStore = (IInfoStore) session.getService("InfoStore");
// 1. SAPR3 계정 전체 조회
List<IUser> sapr3Users = fetchSapr3Users(infoStore);
LOG.info("SAPR3 계정 총 " + sapr3Users.size() + "건 조회 완료");
// 2. 전체 AccountName 집합 (중복 체크용) - 소문자 정규화
Set<String> allAccountNames = fetchAllAccountNames(infoStore);
// 3. 이메일별 계정 그룹핑 (이메일 중복 여부 파악)
Map<String, List<IUser>> emailToUsers = groupByEmail(sapr3Users);
// 4. 각 계정 처리
for (IUser user : sapr3Users) {
try {
processUser(user, emailToUsers, allAccountNames, infoStore);
} catch (Exception e) {
cntError++;
LOG.warning("처리 오류 [SI_ID=" + user.getID()
+ ", AccountName=" + user.getAccountName() + "]: " + e.getMessage());
}
}
printSummary();
} catch (SDKException e) {
LOG.severe("BO SDK 오류: " + e.getMessage());
e.printStackTrace();
} finally {
if (session != null) {
session.logoff();
LOG.info("BO 세션 로그오프 완료");
}
}
}
// ── 로그인 ────────────────────────────────────────────────────────────
private IEnterpriseSession login() throws SDKException {
ISessionMgr sessionMgr = CrystalEnterprise.getSessionMgr();
return sessionMgr.logon(BO_USER, BO_PASSWORD, CMS_NAME, AUTH_TYPE);
}
// ── SAPR3 계정 조회 ───────────────────────────────────────────────────
private List<IUser> fetchSapr3Users(IInfoStore infoStore) throws SDKException {
// SI_USERKIND=1: 일반 사용자, authentication 필터는 쿼리 후 코드에서 처리
// (BO SDK에서 인증 유형 직접 쿼리가 제한적일 수 있어 전체 조회 후 필터링)
String query = "SELECT SI_ID, SI_NAME, SI_EMAIL_ADDRESS, SI_USERKIND "
+ "FROM CI_SYSTEMOBJECTS "
+ "WHERE SI_PROGID='CrystalEnterprise.UserInfo' "
+ "AND SI_USERKIND=1";
IInfoObjects infoObjects = (IInfoObjects) infoStore.query(query);
List<IUser> result = new ArrayList<>();
for (int i = 0; i < infoObjects.size(); i++) {
IInfoObject obj = (IInfoObject) infoObjects.get(i);
if (obj instanceof IUser) {
IUser user = (IUser) obj;
// SAPR3 인증 방식만 필터링
if (SAPR3_AUTH.equalsIgnoreCase(user.getAuthType())) {
result.add(user);
}
}
}
return result;
}
// ── 전체 AccountName 집합 조회 ────────────────────────────────────────
private Set<String> fetchAllAccountNames(IInfoStore infoStore) throws SDKException {
String query = "SELECT SI_ID, SI_NAME "
+ "FROM CI_SYSTEMOBJECTS "
+ "WHERE SI_PROGID='CrystalEnterprise.UserInfo' "
+ "AND SI_USERKIND=1";
IInfoObjects infoObjects = (IInfoObjects) infoStore.query(query);
Set<String> names = new HashSet<>();
for (int i = 0; i < infoObjects.size(); i++) {
IInfoObject obj = (IInfoObject) infoObjects.get(i);
if (obj instanceof IUser) {
String accountName = ((IUser) obj).getAccountName();
if (accountName != null) {
names.add(accountName.toLowerCase(Locale.ROOT));
}
}
}
return names;
}
// ── 이메일 기준 그룹핑 ────────────────────────────────────────────────
private Map<String, List<IUser>> groupByEmail(List<IUser> users) {
Map<String, List<IUser>> map = new HashMap<>();
for (IUser user : users) {
String email = getEmail(user);
if (email != null && !email.isEmpty()) {
map.computeIfAbsent(email.toLowerCase(Locale.ROOT), k -> new ArrayList<>()).add(user);
}
}
return map;
}
// ── 단일 계정 처리 ────────────────────────────────────────────────────
private void processUser(IUser user,
Map<String, List<IUser>> emailToUsers,
Set<String> allAccountNames,
IInfoStore infoStore) throws SDKException {
String currentAccountName = user.getAccountName();
String email = getEmail(user);
int siId = user.getID();
// 이메일이 없는 계정은 처리 불가 → 스킵
if (email == null || email.isEmpty() || !email.contains("@")) {
LOG.info(String.format("[SKIP-NO-EMAIL] SI_ID=%d, AccountName=%s", siId, currentAccountName));
cntSkipped++;
return;
}
String emailLocalPart = email.substring(0, email.indexOf('@')).toLowerCase(Locale.ROOT);
String biSiId = "BI" + siId;
// 이미 AccountName == 이메일 로컬파트 → 변경 불필요
if (emailLocalPart.equals(currentAccountName.toLowerCase(Locale.ROOT))) {
LOG.fine(String.format("[NO-CHANGE] SI_ID=%d, AccountName=%s (이미 일치)", siId, currentAccountName));
return;
}
// ── 규칙 3: 이메일 중복 + AccountName이 이미 BI+SI_ID → 스킵 ──────
List<IUser> sameEmailUsers = emailToUsers.getOrDefault(email.toLowerCase(Locale.ROOT), Collections.emptyList());
boolean isDuplicateEmail = sameEmailUsers.size() > 1;
boolean isAlreadyBiSiId = biSiId.equalsIgnoreCase(currentAccountName);
if (isDuplicateEmail && isAlreadyBiSiId) {
LOG.info(String.format(
"[SKIP-DUP-EMAIL+BI_SIID] SI_ID=%d, AccountName=%s, Email=%s",
siId, currentAccountName, email));
cntSkipped++;
return;
}
// ── 규칙 2: emailLocalPart가 이미 다른 계정에 존재하면 → BI+SI_ID ──
String newAccountName;
if (allAccountNames.contains(emailLocalPart)
&& !emailLocalPart.equals(currentAccountName.toLowerCase(Locale.ROOT))) {
// 단, BI+SI_ID 도 이미 존재하면 경고 후 스킵
if (allAccountNames.contains(biSiId.toLowerCase(Locale.ROOT))
&& !biSiId.equalsIgnoreCase(currentAccountName)) {
LOG.warning(String.format(
"[SKIP-CONFLICT] SI_ID=%d, 목표 AccountName=%s 및 BI+SI_ID=%s 모두 충돌",
siId, emailLocalPart, biSiId));
cntSkipped++;
return;
}
newAccountName = biSiId;
LOG.info(String.format(
"[FALLBACK] SI_ID=%d, %s → %s (emailLocalPart 충돌, BI+SI_ID 적용)",
siId, currentAccountName, newAccountName));
} else {
// ── 규칙 1: emailLocalPart로 변경 ─────────────────────────────
newAccountName = emailLocalPart;
LOG.info(String.format(
"[RENAME] SI_ID=%d, %s → %s",
siId, currentAccountName, newAccountName));
}
// ── 실제 변경 적용 ────────────────────────────────────────────────
applyRename(user, newAccountName, currentAccountName, allAccountNames, infoStore);
}
// ── BO SDK AccountName 변경 및 저장 ──────────────────────────────────
private void applyRename(IUser user,
String newAccountName,
String oldAccountName,
Set<String> allAccountNames,
IInfoStore infoStore) throws SDKException {
try {
user.setAccountName(newAccountName);
// IInfoObjects 래퍼에 담아 commit
IInfoObjects container = infoStore.newInfoObjectCollection();
container.add(user);
infoStore.commit(container);
// 메모리 내 집합 갱신 (이후 처리에 반영)
allAccountNames.remove(oldAccountName.toLowerCase(Locale.ROOT));
allAccountNames.add(newAccountName.toLowerCase(Locale.ROOT));
if (newAccountName.startsWith("BI")) {
cntFallback++;
} else {
cntChanged++;
}
} catch (SDKException e) {
cntError++;
LOG.severe(String.format(
"[ERROR] SI_ID=%d, %s → %s 변경 실패: %s",
user.getID(), oldAccountName, newAccountName, e.getMessage()));
throw e;
}
}
// ── 이메일 추출 헬퍼 ─────────────────────────────────────────────────
private String getEmail(IUser user) {
try {
return user.getEmailAddress(); // IUser.getEmailAddress() 또는 SI_EMAIL_ADDRESS
} catch (Exception e) {
return null;
}
}
// ── 처리 결과 요약 출력 ───────────────────────────────────────────────
private void printSummary() {
LOG.info("========================================");
LOG.info("처리 결과 요약");
LOG.info(" 정상 변경 (emailLocalPart) : " + cntChanged + "건");
LOG.info(" Fallback 변경 (BI+SI_ID) : " + cntFallback + "건");
LOG.info(" 스킵 : " + cntSkipped + "건");
LOG.info(" 오류 : " + cntError + "건");
LOG.info("========================================");
}
}