/* * Copyright 2000-2013 Enonic AS * http://www.enonic.com/license */ package com.enonic.cms.core.vacuum; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.support.JdbcUtils; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import com.enonic.cms.framework.blob.gc.GarbageCollector; import com.enonic.cms.core.security.SecurityService; import com.enonic.cms.core.security.user.User; import com.enonic.cms.core.security.userstore.MemberOfResolver; import com.enonic.cms.store.support.ConnectionFactory; @Component public class VacuumServiceImpl implements VacuumService { private static final int BATCH_SIZE = 10; private static final String VACUUM_READ_LOGS_SQL = "DELETE FROM tLogEntry WHERE len_lTypeKey = 7"; @Autowired protected GarbageCollector garbageCollector; @Autowired protected ConnectionFactory connectionFactory; @Autowired protected SecurityService securityService; @Autowired protected MemberOfResolver memberOfResolver; private ProgressInfo progressInfo = new ProgressInfo(); private final Map<String, List<Integer>> queryCache = new HashMap<String, List<Integer>>(); /** * Clean read logs. */ @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public void cleanReadLogs() { if ( progressInfo.isInProgress() || !isAdmin() ) { return; } try { startProgress( "Cleaning read logs..." ); final Connection conn = connectionFactory.getConnection( true ); setProgress( "Vacuum read logs...", 5 ); vacuumReadLogs( conn ); } catch ( final Exception e ) { setProgress( "Failed to clean read logs: " + e.getMessage(), 100 ); progressInfo.setInProgress( false ); throw new RuntimeException( "Failed to clean read logs", e ); } finally { finishProgress(); } } /** * Clean unused content. */ @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class, timeout = 3600) public void cleanUnusedContent() { if ( progressInfo.isInProgress() || !isAdmin() ) { return; } try { startProgress( "Cleaning unused content..." ); final Connection conn = connectionFactory.getConnection( true ); setProgress( "Vacuum binaries...", 5 ); vacuumBinaries( conn ); setProgress( "Vacuum contents...", 20 ); vacuumContents( conn ); setProgress( "Vacuum categories...", 40 ); vacuumCategories( conn ); setProgress( "Vacuum archives...", 60 ); vacuumArchives( conn ); setProgress( "Vacuum blob store...", 80 ); vacuumBlobStore(); } catch ( final Exception e ) { setProgress( "Failed to clean unused content: " + e.getMessage(), 100 ); progressInfo.setInProgress( false ); throw new RuntimeException( "Failed to clean unused content", e ); } finally { finishProgress(); queryCache.clear(); } } /** * returns progress info about either Clean unused content or Clean read logs. */ public ProgressInfo getProgressInfo() { if ( !isAdmin() ) { return ProgressInfo.NONE; } return progressInfo; } private void startProgress( final String logLine ) { setProgress( logLine, 0 ); progressInfo.setInProgress( true ); } private void setProgress( final String logLine, final int percent ) { progressInfo.setLogLine( logLine ); progressInfo.setPercent( percent ); } private void finishProgress() { if ( progressInfo.isInProgress() ) { setProgress( "Finished. Last job was executed at " + new Date().toString(), 100 ); progressInfo.setInProgress( false ); } } private boolean isAdmin() { final User user = securityService.getLoggedInAdminConsoleUser(); return memberOfResolver.hasEnterpriseAdminPowers( user.getKey() ); } /** * Vacuum binaries. */ private void vacuumBinaries( final Connection conn ) throws Exception { executeStatements( conn, VacuumContentSQL.VACUUM_BINARIES_STATEMENTS ); } /** * Vacuum contents. */ private void vacuumContents( final Connection conn ) throws Exception { executeStatements( conn, VacuumContentSQL.VACUUM_CONTENT_STATEMENTS ); } /** * Vacuum categories. */ private void vacuumCategories( final Connection conn ) throws Exception { executeStatements( conn, VacuumContentSQL.VACUUM_CATEGORIES_STATEMENTS ); } /** * Vacuum arvhives. */ private void vacuumArchives( final Connection conn ) throws Exception { executeStatements( conn, VacuumContentSQL.VACUUM_ARCHIVES_STATEMENTS ); } /** * Vacuum read logs. */ private void vacuumReadLogs( final Connection conn ) throws Exception { executeStatements( conn, new String[]{VACUUM_READ_LOGS_SQL} ); } private void vacuumBlobStore() { this.garbageCollector.process(); } /** * Execute a list of statements. */ private void executeStatements( final Connection conn, final String[] sqlList ) throws Exception { for ( final String sql : sqlList ) { executeStatementBatch( conn, sql ); } } /** * Execute statement. */ private void executeStatement( final Connection conn, final String sql ) throws Exception { Statement stmt = null; try { stmt = conn.createStatement(); stmt.execute( sql ); } finally { JdbcUtils.closeStatement( stmt ); } } /** * Execute statement trying do it in batch. */ private void executeStatementBatch( final Connection conn, final String sql ) throws Exception { final Pattern pattern = Pattern.compile( "DELETE FROM (\\w+) WHERE (\\w+) IN \\((.*)\\)" ); final Matcher matcher = pattern.matcher( sql ); if ( matcher.matches() ) { final String table = matcher.group( 1 ); final String column = matcher.group( 2 ); final String select = matcher.group( 3 ); final List<Integer> ids = readIds( conn, select ); deleteIdsInTableBatch( conn, ids, column, table ); } else { // WHERE NOT IN query or some unknown ... -> just execute SQL. executeStatement( conn, sql ); } } /** * gets array of primary keys that are to delete. used query cache. */ private List<Integer> readIds( final Connection conn, final String select ) throws SQLException { List<Integer> ids = queryCache.get( select ); if ( ids == null ) { ids = readIdsFromDB( conn, select ); queryCache.put( select, ids ); } return ids; } /** * gets array of primary keys that are to delete. read from database */ private List<Integer> readIdsFromDB( final Connection conn, final String select ) throws SQLException { final List<Integer> ids = new ArrayList<Integer>(); Statement stmt = null; ResultSet rs = null; try { stmt = conn.createStatement(); rs = stmt.executeQuery( select ); while ( rs.next() ) { ids.add( rs.getInt( 1 ) ); } } finally { JdbcUtils.closeResultSet( rs ); JdbcUtils.closeStatement( stmt ); } return ids; } /** * splits delete to batch */ private void deleteIdsInTableBatch( final Connection conn, final List<Integer> ids, final String column, final String table ) throws Exception { int fromIndex, length; for ( fromIndex = 0, length = ids.size(); fromIndex < length; fromIndex += BATCH_SIZE ) { final int toIndex = fromIndex + BATCH_SIZE < length ? fromIndex + BATCH_SIZE : length; deleteIdsInTable( conn, ids.subList( fromIndex, toIndex ), column, table ); } } /** * deletes from table by idsds */ private void deleteIdsInTable( final Connection conn, final List<Integer> ids, final String column, final String table ) throws Exception { final String join = StringUtils.join( ids, "," ); final String sql = "DELETE FROM " + table + " WHERE " + column + " IN (" + join + ")"; executeStatement( conn, sql ); } }