001package com.killcoding.datasource;
002
003import java.io.File;
004import com.killcoding.log.Logger;
005import com.killcoding.log.LoggerFactory;
006import com.killcoding.datasource.DriverDataSource;
007import com.killcoding.tool.ConfigProperties;
008import com.killcoding.tool.CommonTools;
009import java.sql.Connection;
010import java.sql.SQLException;
011import com.killcoding.datasource.CacheDriverExecutor;
012import java.util.Map;
013import java.util.HashMap;
014import com.killcoding.datasource.Clock;
015import java.sql.Timestamp;
016import java.util.concurrent.ConcurrentHashMap;
017
018/**
019 * 
020    CREATE TABLE identity_card (
021     id CHAR(16),
022     identity_name VARCHAR(200),
023     identity_value BIGINT,
024     generate_task_id CHAR(16),
025     created_user_id CHAR(16),
026     updated_user_id CHAR(16),
027     deleted_user_id CHAR(16), 
028     created_at DATETIME(6),
029     updated_at DATETIME(6),
030     deleted_at DATETIME(6),
031     deleted TINYINT(1)
032    ) ENGINE=InnoDB;
033    
034    ALTER TABLE identity_card ADD PRIMARY KEY (id);
035    ALTER TABLE identity_card ADD UNIQUE INDEX idx_idcard_unique_idname (identity_name);
036    ALTER TABLE identity_card ADD INDEX idx_idcard_deleted (deleted);
037 *
038 * */
039public final class IdentityCard {
040    
041    private final static Map<String, Integer> CURRENT_VALUES = new ConcurrentHashMap<String, Integer>();
042    private final static Map<String, Long> STEP_LAST_VALUES = new ConcurrentHashMap<String, Long>();
043
044        private static final String SQL_INSERT = "INSERT INTO %s (id,identity_name,identity_value,generate_task_id,created_user_id,updated_user_id,deleted_user_id,created_at,updated_at,deleted_at,deleted) VALUES (:id,:identity_name,:identity_value,:generate_task_id,:created_user_id,:updated_user_id,NULL,:created_at,:updated_at,:deleted_at,:deleted)";
045
046        private static final String SQL_UPDATE = "UPDATE %s SET identity_value = identity_value + :step_value,generate_task_id = :generate_task_id,updated_user_id = :updated_user_id,updated_at = :updated_at WHERE id = :id AND (generate_task_id = '0' OR updated_at <= :timeout_updated_at)";
047
048        private static final String SQL_RESET = "UPDATE %s SET identity_value = :identity_value,updated_user_id = :updated_user_id,updated_at = :updated_at WHERE id = :id AND generate_task_id = :generate_task_id";
049
050        private static final String SQL_RESET_FORCE = "UPDATE %s SET identity_value = :identity_value,updated_user_id = :updated_user_id,updated_at = :updated_at WHERE id = :id";
051
052        private static final String SQL_CLEAR = "UPDATE %s SET generate_task_id = '0',updated_user_id = :updated_user_id,updated_at = :updated_at WHERE id = :id AND generate_task_id = :generate_task_id";
053
054        private static final String SQL_FIRST = "SELECT * FROM %s WHERE deleted = 0 AND identity_name = :identity_name";
055
056        private static final String SQL_FIRST_BY_GENERATE_TASK_ID = "SELECT * FROM %s WHERE deleted = 0 AND identity_name = :identity_name AND generate_task_id = :generate_task_id";
057
058        public static File APP_IDENTITY_CARD = null;
059
060        private Logger log = LoggerFactory.getLogger(IdentityCard.class);
061
062        private CacheDriverExecutor driverExecutor = null;
063        
064        private static Long MAX_VALUE_LIMITED = Long.MAX_VALUE - 1;
065        private static Long MIN_VALUE_LIMITED = 1L;
066        private static Integer STEP_VALUE_LIMITED = 0;
067
068        private static boolean enable = false;
069        private static ConfigProperties configProperties = null;
070        private static DriverDataSource dataSource = null;
071        private static String dataSourcePath = null;
072        private static String tableName = null;
073        private static Long timeout = 10000L;
074        private static Long maxValue = MAX_VALUE_LIMITED;
075        private static Long minValue = MIN_VALUE_LIMITED;
076        private static Integer stepValue = STEP_VALUE_LIMITED;
077
078        private String identityName = null;
079        private String generateTaskId = null;
080        private Object userId = null;
081
082        public IdentityCard(String identityName) {
083                super();
084                this.identityName = identityName;
085        initValues();
086                init();
087        }
088        
089        private synchronized void initValues(){
090            if(!CURRENT_VALUES.containsKey(identityName)){
091                CURRENT_VALUES.put(identityName,0);
092                STEP_LAST_VALUES.put(identityName,0L);
093            }
094        }
095
096        private void initConn() throws SQLException {
097                Connection conn = dataSource.getConnection();
098                conn.setAutoCommit(false);
099                driverExecutor = new CacheDriverExecutor(conn);
100        }
101        
102        public synchronized Long generateId() throws SQLException {
103            if (!enable)
104                        return null;
105                
106                if(CURRENT_VALUES.get(identityName) <= 0){      
107                    STEP_LAST_VALUES.put(identityName,generateIdForNextStep());
108                    CURRENT_VALUES.put(identityName,stepValue);
109                }
110                
111                Long val = STEP_LAST_VALUES.get(identityName) - CURRENT_VALUES.get(identityName);
112                CURRENT_VALUES.put(identityName,CURRENT_VALUES.get(identityName) - 1);
113                return val;
114        }
115
116        public synchronized Long generateIdForNextStep() throws SQLException {
117
118                if (!enable)
119                        return null;
120
121                Long nextValue = null;
122                try {
123                        initConn();
124                        Object id = null;
125                        generateTaskId = CommonTools.generateId(16);
126                        Map<String, Object> item = first();
127                        if (item == null) {
128                                id = create();
129                                item = first();
130                        }
131                        id = item.get("id");
132                        boolean updated = update(id);
133                        if (updated) {
134                                item = firstByGenerateTaskId();
135                                nextValue = Long.parseLong(item.get("identity_value").toString());
136                        }
137                        if (nextValue != null) {
138                                if (nextValue > maxValue) {
139                                    nextValue = minValue + stepValue;
140                                        reset(id,nextValue);
141                                }
142                        }
143                        clear(id);
144                        driverExecutor.commit();
145                        if (nextValue == null) {
146                                int errorCode = 99800;
147                                String errorMsg = String.format("The '%s' next value is null.", identityName);
148                                log.warn(String.format("ERROR CODE: (%s) %s", errorCode, errorMsg));
149                                throw new SQLException(errorMsg) {
150                                        @Override
151                                        public int getErrorCode() {
152                                                return errorCode;
153                                        }
154                                };
155                        }
156                        return nextValue;
157                } catch (Exception e) {
158                        log.warn(e.getMessage(), e);
159
160                        if (driverExecutor != null)
161                                driverExecutor.rollback();
162                                
163                int errorCode = 99802;
164                String errorMsg = e.getMessage();
165                log.warn(String.format("ERROR CODE: (%s) %s", errorCode, errorMsg));
166                throw new SQLException(errorMsg) {
167                        @Override
168                        public int getErrorCode() {
169                                return errorCode;
170                        }
171                };                              
172
173                } finally {
174                        if (driverExecutor != null){
175                                driverExecutor.close();
176                        }
177                }
178        }
179
180        public synchronized String generateId(int length) throws SQLException {
181                if (!enable)
182                        return null;
183                        
184                if(length < 1){
185                int errorCode = 99801;
186                String errorMsg = String.format("The value length '%s' cannot be less than 1.", length);
187                log.warn(String.format("ERROR CODE: (%s) %s", errorCode, errorMsg));
188                throw new SQLException(errorMsg) {
189                        @Override
190                        public int getErrorCode() {
191                                return errorCode;
192                        }
193                };      
194                }
195                
196                Long val = generateId();
197                if(val > getMaxValueForLength(length)){
198                    Map<String,Object> item = first();
199                        Object id = item.get("id");
200                    resetForce(id);
201                    val = minValue;
202                }
203                if (val == null)
204                        return null;
205
206                return String.format("%0" + length + "d", val);
207        }
208
209        public synchronized String generateId(int length, String prefix) throws SQLException {
210                if (!enable)
211                        return null;
212                        
213                int valLen = length - prefix.length();
214                return String.format("%s%s", prefix, generateId(valLen));
215        }
216        
217        public synchronized String generateId(int length, String prefix, String delimiter,int randomSuffixLength) throws SQLException {
218                if (!enable)
219                        return null;
220                        
221            if(CommonTools.isBlank(delimiter)){
222                delimiter = "";
223            }
224            String suffix = "";
225            if(randomSuffixLength > 0){
226                suffix = String.format("%s%s",delimiter,CommonTools.generateId(randomSuffixLength).toUpperCase());
227            }
228            int valLen = length - prefix.length() - delimiter.length() - suffix.length();
229            return String.format("%s%s%s%s", prefix,delimiter,generateId(valLen),suffix).substring(0,length);
230        }
231
232        private synchronized void init() {
233                if (dataSource == null) {
234                        try {
235                                if (APP_IDENTITY_CARD == null) {
236                                        APP_IDENTITY_CARD = CommonTools.getSystemProperties(IdentityCard.class, "APP_IDENTITY_CARD",
237                                                        "IdentityCard.properties");
238                                }
239                                if (APP_IDENTITY_CARD != null && APP_IDENTITY_CARD.exists() && configProperties == null) {
240                                        configProperties = new ConfigProperties();
241                                        Runnable updatedRun = new Runnable() {
242                                                @Override
243                                                public void run() {
244                                                        try {
245                                                                reload();
246                                                                Logger.systemMark(IdentityCard.class, "IdentityCard Reloaded");
247                                                        } catch (Exception e) {
248                                                                log.error(e.getMessage(), e);
249                                                        }
250                                                }
251                                        };
252
253                                        configProperties.load(APP_IDENTITY_CARD, updatedRun);
254                                        enable = configProperties.getBoolean("Enable", false);
255                                        tableName = configProperties.getString("TableName", "identity_card");
256                                        timeout = configProperties.getMilliSeconds("Timeout", 10000L);
257                                        maxValue = configProperties.getLong("MaxValue", MAX_VALUE_LIMITED);
258                                        minValue = configProperties.getLong("MinValue", MIN_VALUE_LIMITED);
259                                        stepValue = configProperties.getInteger("StepValue", STEP_VALUE_LIMITED);
260                                        
261                                if(maxValue > MAX_VALUE_LIMITED) maxValue = MAX_VALUE_LIMITED;
262                                
263                                if(minValue < MIN_VALUE_LIMITED) minValue = MIN_VALUE_LIMITED;
264                                
265                                if(stepValue < STEP_VALUE_LIMITED) stepValue = STEP_VALUE_LIMITED;
266                                
267                                        if (enable) {
268                                                dataSourcePath = configProperties.getString("DataSource");
269                                                if (!CommonTools.isBlank(dataSourcePath)) {
270                                                        dataSource = new DriverDataSource(new File(dataSourcePath));
271                                                }
272                                        }
273                                }
274                        } catch (Exception e) {
275                                log.error(e.getMessage(), e);
276                        }
277                }
278        }
279
280        private void reload() {
281                if (configProperties != null) {
282                        enable = configProperties.getBoolean("Enable", false);
283                        tableName = configProperties.getString("TableName", "identity_card");
284                        timeout = configProperties.getMilliSeconds("Timeout", 10000L);
285                        String reloadDataSourcePath = configProperties.getString("DataSource");
286                        
287                        maxValue = configProperties.getLong("MaxValue", MAX_VALUE_LIMITED);
288                        minValue = configProperties.getLong("MinValue", MIN_VALUE_LIMITED);
289                        stepValue = configProperties.getInteger("StepValue", STEP_VALUE_LIMITED);
290                        
291                        if(maxValue > MAX_VALUE_LIMITED) maxValue = MAX_VALUE_LIMITED;
292                        
293                        if(minValue < MIN_VALUE_LIMITED) minValue = MIN_VALUE_LIMITED;
294                        
295                        if(stepValue < STEP_VALUE_LIMITED) stepValue = STEP_VALUE_LIMITED;                      
296
297                        boolean isReload = false;
298                        if (!CommonTools.isBlank(reloadDataSourcePath)) {
299                                isReload = !dataSourcePath.equals(reloadDataSourcePath);
300                        }
301                        if (dataSource != null && isReload) {
302                            // May be referenced when expansion is needed
303                                // dataSource.closeAll();
304                                dataSourcePath = reloadDataSourcePath;
305                        }
306                        // May be referenced when expansion is needed
307                        // if (!enable && dataSource != null) {
308                        //       dataSource.closeAll();
309                        // }
310                        if (enable && isReload) {
311                                if (!CommonTools.isBlank(dataSourcePath)) {
312                                        dataSource = new DriverDataSource(new File(dataSourcePath));
313                                }
314                        }
315                }
316        }
317
318        private boolean update(Object id) throws SQLException {
319                Clock clock = new Clock();
320                Timestamp currentTime = clock.getSqlTimestamp();
321                String sql = formatSql(SQL_UPDATE);
322                Map<String, Object> params = new HashMap<String, Object>();
323                params.put("id", id);
324        params.put("step_value",stepValue);
325                params.put("generate_task_id", generateTaskId);
326                params.put("updated_user_id", userId);
327                params.put("updated_at", currentTime);
328                params.put("timeout_updated_at", new Timestamp(currentTime.getTime() - timeout));
329                return driverExecutor.execute(sql, params) > 0;
330        }
331
332        private void clear(Object id) throws SQLException {
333                Clock clock = new Clock();
334                Timestamp currentTime = clock.getSqlTimestamp();
335                String sql = formatSql(SQL_CLEAR);
336                Map<String, Object> params = new HashMap<String, Object>();
337                params.put("id", id);
338                params.put("generate_task_id", generateTaskId);
339                params.put("updated_user_id", userId);
340                params.put("updated_at", currentTime);
341                driverExecutor.execute(sql, params);
342        }
343
344        private void reset(Object id,Long resetValue) throws SQLException {
345                Clock clock = new Clock();
346                Timestamp currentTime = clock.getSqlTimestamp();
347                String sql = formatSql(SQL_RESET);
348                Map<String, Object> params = new HashMap<String, Object>();
349                params.put("id", id);
350                params.put("identity_value", resetValue);
351                params.put("generate_task_id", generateTaskId);
352                params.put("updated_user_id", userId);
353                params.put("updated_at", currentTime);
354                driverExecutor.execute(sql, params);
355        }
356        
357        private void resetForce(Object id) throws SQLException {
358                Clock clock = new Clock();
359                Timestamp currentTime = clock.getSqlTimestamp();
360                String sql = formatSql(SQL_RESET_FORCE);
361                Map<String, Object> params = new HashMap<String, Object>();
362                params.put("id", id);
363                params.put("identity_value", minValue);
364                params.put("updated_user_id", userId);
365                params.put("updated_at", currentTime);
366                driverExecutor.execute(sql, params);
367        }       
368
369        private Object create() throws SQLException {
370                Object id = CommonTools.generateId(16);
371                Clock clock = new Clock();
372                Timestamp currentTime = clock.getSqlTimestamp();
373                Long createIdentityValue = minValue;
374                
375                if(createIdentityValue < 0)  createIdentityValue = 0L;
376                
377                String sql = formatSql(SQL_INSERT);
378                Map<String, Object> params = new HashMap<String, Object>();
379                params.put("id", id);
380                params.put("identity_name", identityName);
381                params.put("identity_value", createIdentityValue);
382                params.put("generate_task_id", "0");
383                params.put("created_user_id", userId);
384                params.put("updated_user_id", userId);
385                params.put("deleted_user_id", null);
386                params.put("created_at", currentTime);
387                params.put("updated_at", currentTime);
388                params.put("deleted_at", null);
389                params.put("deleted", 0);
390                driverExecutor.execute(sql, params);
391                return id;
392        }
393
394        private Map<String, Object> first() throws SQLException {
395                String sql = formatSql(SQL_FIRST);
396                Map<String, Object> params = new HashMap<String, Object>();
397                params.put("identity_name", identityName);
398                return driverExecutor.first(sql, params);
399        }
400
401        private Map<String, Object> firstByGenerateTaskId() throws SQLException {
402                String sql = formatSql(SQL_FIRST_BY_GENERATE_TASK_ID);
403                Map<String, Object> params = new HashMap<String, Object>();
404                params.put("identity_name", identityName);
405                params.put("generate_task_id", generateTaskId);
406                return driverExecutor.first(sql, params);
407        }
408        
409        private Long getMaxValueForLength(int length) {
410            String strValue = String.format("%0" + length + "d",0).replaceAll("0","9");
411            return Long.parseLong(strValue);
412        }
413
414        public static DriverDataSource getDataSource() {
415                return dataSource;
416        }
417
418        public String formatSql(String sql) {
419                return String.format(sql, tableName);
420        }
421        
422        public String formatSql(String sql,Integer stepValue) {
423                return String.format(sql, tableName,stepValue);
424        }       
425
426        public void setUserId(Object userId) {
427                this.userId = userId;
428        }
429
430        public Object getUserId() {
431                return this.userId;
432        }
433
434        public String getIdentityName() {
435                return identityName;
436        }
437        
438        public static ConfigProperties getConfigProperties(){
439            return configProperties;
440        }       
441}