카테고리 없음

BoBwAccountRename

anythingsay 2026. 5. 5. 19:28

<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("========================================");
    }
}