/* * Created on Sep 4, 2013 * Created by Paul Gardner * * Copyright 2013 Azureus Software, Inc. All rights reserved. * * 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; version 2 of the License only. * * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. */ package com.aelitis.azureus.core.tag.impl; import java.util.*; import java.util.regex.Pattern; import org.gudy.azureus2.core3.download.DownloadManager; import org.gudy.azureus2.core3.torrent.TOTorrent; import org.gudy.azureus2.core3.util.AERunnable; import org.gudy.azureus2.core3.util.AsyncDispatcher; import org.gudy.azureus2.core3.util.Debug; import org.gudy.azureus2.core3.util.SimpleTimer; import org.gudy.azureus2.core3.util.SystemTime; import org.gudy.azureus2.core3.util.TimerEvent; import org.gudy.azureus2.core3.util.TimerEventPerformer; import org.gudy.azureus2.core3.util.TimerEventPeriodic; import com.aelitis.azureus.core.AzureusCore; import com.aelitis.azureus.core.AzureusCoreFactory; import com.aelitis.azureus.core.AzureusCoreRunningListener; import com.aelitis.azureus.core.tag.Tag; import com.aelitis.azureus.core.tag.TagFeatureProperties; import com.aelitis.azureus.core.tag.TagFeatureProperties.TagProperty; import com.aelitis.azureus.core.tag.TagFeatureProperties.TagPropertyListener; import com.aelitis.azureus.core.tag.TagListener; import com.aelitis.azureus.core.tag.TagType; import com.aelitis.azureus.core.tag.TagTypeListener; import com.aelitis.azureus.core.tag.Taggable; import com.aelitis.azureus.core.tag.TaggableLifecycleListener; public class TagPropertyConstraintHandler implements TagTypeListener { private final AzureusCore azureus_core; private final TagManagerImpl tag_manager; private boolean initialised; private boolean initial_assignment_complete; private Map<Tag,TagConstraint> constrained_tags = new HashMap<Tag,TagConstraint>(); private Map<Tag,Map<DownloadManager,Long>> apply_history = new HashMap<Tag, Map<DownloadManager,Long>>(); private AsyncDispatcher dispatcher = new AsyncDispatcher( "tag:constraints" ); private TimerEventPeriodic timer; private TagPropertyConstraintHandler() { azureus_core = null; tag_manager = null; } protected TagPropertyConstraintHandler( AzureusCore _core, TagManagerImpl _tm ) { azureus_core = _core; tag_manager = _tm; tag_manager.addTaggableLifecycleListener( Taggable.TT_DOWNLOAD, new TaggableLifecycleListener() { public void initialised( List<Taggable> current_taggables ) { try{ TagType tt = tag_manager.getTagType( TagType.TT_DOWNLOAD_MANUAL ); tt.addTagTypeListener( TagPropertyConstraintHandler.this, true ); }finally{ AzureusCoreFactory.addCoreRunningListener( new AzureusCoreRunningListener() { public void azureusCoreRunning( AzureusCore core ) { synchronized( constrained_tags ){ initialised = true; apply( core.getGlobalManager().getDownloadManagers(), true ); } } }); } } public void taggableCreated( Taggable taggable ) { apply((DownloadManager)taggable, null, false ); } public void taggableDestroyed( Taggable taggable ) { } }); } public void tagTypeChanged( TagType tag_type ) { } public void tagAdded( Tag tag ) { TagFeatureProperties tfp = (TagFeatureProperties)tag; TagProperty prop = tfp.getProperty( TagFeatureProperties.PR_CONSTRAINT ); if ( prop != null ){ prop.addListener( new TagPropertyListener() { public void propertyChanged( TagProperty property ) { handleProperty( property ); } public void propertySync( TagProperty property ) { } }); handleProperty( prop ); } tag.addTagListener( new TagListener() { public void taggableSync( Tag tag ) { } public void taggableRemoved( Tag tag, Taggable tagged ) { apply((DownloadManager)tagged, tag, true ); } public void taggableAdded( Tag tag, Taggable tagged ) { apply((DownloadManager)tagged, tag, true ); } }, false ); } public void tagChanged( Tag tag ) { } private void checkTimer() { if ( constrained_tags.size() > 0 ){ if ( timer == null ){ timer = SimpleTimer.addPeriodicEvent( "tag:constraint:timer", 30*1000, new TimerEventPerformer() { public void perform( TimerEvent event) { apply_history.clear(); apply(); } }); } }else if ( timer != null ){ timer.cancel(); timer = null; apply_history.clear(); } } public void tagRemoved( Tag tag ) { synchronized( constrained_tags ){ if ( constrained_tags.containsKey( tag )){ constrained_tags.remove( tag ); checkTimer(); } } } private void handleProperty( TagProperty property ) { Tag tag = property.getTag(); synchronized( constrained_tags ){ String[] temp = property.getStringList(); String constraint = temp == null || temp.length < 1?"":temp[0].trim(); if ( constraint.length() == 0 ){ if ( constrained_tags.containsKey( tag )){ constrained_tags.remove( tag ); } }else{ TagConstraint con = constrained_tags.get( tag ); if ( con != null && con.getConstraint().equals( constraint )){ return; } Set<Taggable> existing = tag.getTagged(); for ( Taggable e: existing ){ tag.removeTaggable( e ); } con = new TagConstraint( tag, constraint ); constrained_tags.put( tag, con ); if ( initialised ){ apply( con ); } } checkTimer(); } } private void apply( final DownloadManager dm, Tag related_tag, boolean auto ) { if ( dm.isDestroyed()){ return; } synchronized( constrained_tags ){ if ( constrained_tags.size() == 0 || !initialised ){ return; } if ( auto && !initial_assignment_complete ){ return; } } dispatcher.dispatch( new AERunnable() { public void runSupport() { List<TagConstraint> cons; synchronized( constrained_tags ){ cons = new ArrayList<TagConstraint>( constrained_tags.values()); } for ( TagConstraint con: cons ){ con.apply( dm ); } } }); } private void apply( final List<DownloadManager> dms, final boolean initial_assignment ) { synchronized( constrained_tags ){ if ( constrained_tags.size() == 0 || !initialised ){ return; } } dispatcher.dispatch( new AERunnable() { public void runSupport() { List<TagConstraint> cons; synchronized( constrained_tags ){ cons = new ArrayList<TagConstraint>( constrained_tags.values()); } // set up initial constraint tagged state without following implications for ( TagConstraint con: cons ){ con.apply( dms ); } if ( initial_assignment ){ synchronized( constrained_tags ){ initial_assignment_complete = true; } // go over them one more time to pick up consequential constraints for ( TagConstraint con: cons ){ con.apply( dms ); } } } }); } private void apply( final TagConstraint constraint ) { synchronized( constrained_tags ){ if ( !initialised ){ return; } } dispatcher.dispatch( new AERunnable() { public void runSupport() { List<DownloadManager> dms = azureus_core.getGlobalManager().getDownloadManagers(); constraint.apply( dms ); } }); } private void apply() { synchronized( constrained_tags ){ if ( constrained_tags.size() == 0 || !initialised ){ return; } } dispatcher.dispatch( new AERunnable() { public void runSupport() { List<DownloadManager> dms = azureus_core.getGlobalManager().getDownloadManagers(); List<TagConstraint> cons; synchronized( constrained_tags ){ cons = new ArrayList<TagConstraint>( constrained_tags.values()); } for ( TagConstraint con: cons ){ con.apply( dms ); } } }); } private ConstraintExpr compileConstraint( String expr ) { return( new TagConstraint( null, expr ).expr ); } private class TagConstraint { private Tag tag; private String constraint; private ConstraintExpr expr; private TagConstraint( Tag _tag, String _constraint ) { tag = _tag; constraint = _constraint; try{ expr = compileStart( constraint, new HashMap<String,ConstraintExpr>()); }catch( Throwable e ){ Debug.out( "Invalid constraint: " + constraint + " - " + Debug.getNestedExceptionMessage( e )); } } private ConstraintExpr compileStart( String str, Map<String,ConstraintExpr> context ) { str = str.trim(); if ( str.equalsIgnoreCase( "true" )){ return( new ConstraintExprTrue()); } char[] chars = str.toCharArray(); boolean in_quote = false; int level = 0; int bracket_start = 0; StringBuffer result = new StringBuffer( str.length()); for ( int i=0;i<chars.length;i++){ char c = chars[i]; if ( c == '"' ){ if ( i == 0 || chars[i-1] != '\\' ){ in_quote = !in_quote; } } if ( !in_quote ){ if ( c == '(' ){ level++; if ( level == 1 ){ bracket_start = i+1; } }else if ( c == ')' ){ level--; if ( level == 0 ){ String bracket_text = new String( chars, bracket_start, i-bracket_start ).trim(); if ( result.length() > 0 && Character.isLetterOrDigit( result.charAt( result.length()-1 ))){ // function call String key = "{" + context.size() + "}"; context.put( key, new ConstraintExprParams( bracket_text )); result.append( "(" ).append( key ).append( ")" ); }else{ ConstraintExpr sub_expr = compileStart( bracket_text, context ); String key = "{" + context.size() + "}"; context.put(key, sub_expr ); result.append( key ); } } }else if ( level == 0 ){ if ( !Character.isWhitespace( c )){ result.append( c ); } } }else if ( level == 0 ){ result.append( c ); } } if ( level != 0 ){ throw( new RuntimeException( "Unmatched '(' in \"" + str + "\"" )); } if ( in_quote ){ throw( new RuntimeException( "Unmatched '\"' in \"" + str + "\"" )); } return( compileBasic( result.toString(), context )); } private ConstraintExpr compileBasic( String str, Map<String,ConstraintExpr> context ) { if ( str.startsWith( "{" )){ return( context.get( str )); }else if ( str.contains( "||" )){ String[] bits = str.split( "\\|\\|" ); return( new ConstraintExprOr( compile( bits, context ))); }else if ( str.contains( "&&" )){ String[] bits = str.split( "&&" ); return( new ConstraintExprAnd( compile( bits, context ))); }else if ( str.contains( "^" )){ String[] bits = str.split( "\\^" ); return( new ConstraintExprXor( compile( bits, context ))); }else if ( str.startsWith( "!" )){ return( new ConstraintExprNot( compileBasic( str.substring(1).trim(), context ))); }else{ int pos = str.indexOf( '(' ); if ( pos > 0 && str.endsWith( ")" )){ String func = str.substring( 0, pos ); String key = str.substring( pos+1, str.length() - 1 ).trim(); ConstraintExprParams params = (ConstraintExprParams)context.get( key ); return( new ConstraintExprFunction( func, params )); }else{ throw( new RuntimeException( "Unsupported construct: " + str )); } } } private ConstraintExpr[] compile( String[] bits, Map<String,ConstraintExpr> context ) { ConstraintExpr[] res = new ConstraintExpr[ bits.length ]; for ( int i=0; i<bits.length;i++){ res[i] = compileBasic( bits[i].trim(), context ); } return( res ); } private Tag getTag() { return( tag ); } private String getConstraint() { return( constraint ); } private void apply( DownloadManager dm ) { if ( dm.isDestroyed() || !dm.isPersistent()){ return; } if ( expr == null ){ return; } Set<Taggable> existing = tag.getTagged(); if ( testConstraint( dm )){ if ( !existing.contains( dm )){ if( canAddTaggable( dm )){ tag.addTaggable( dm ); } } }else{ if ( existing.contains( dm )){ tag.removeTaggable( dm ); } } } private void apply( List<DownloadManager> dms ) { if ( expr == null ){ return; } Set<Taggable> existing = tag.getTagged(); for ( DownloadManager dm: dms ){ if ( dm.isDestroyed() || !dm.isPersistent()){ continue; } if ( testConstraint( dm )){ if ( !existing.contains( dm )){ if ( canAddTaggable( dm )){ tag.addTaggable( dm ); } } }else{ if ( existing.contains( dm )){ tag.removeTaggable( dm ); } } } } private boolean canAddTaggable( DownloadManager dm ) { long now = SystemTime.getMonotonousTime(); Map<DownloadManager,Long> recent_dms = apply_history.get( tag ); if ( recent_dms != null ){ Long time = recent_dms.get( dm ); if ( time != null && now - time < 1000 ){ System.out.println( "Not applying constraint as too recently actioned: " + dm.getDisplayName() + "/" + tag.getTagName( true )); return( false ); } } if ( recent_dms == null ){ recent_dms = new HashMap<DownloadManager,Long>(); apply_history.put( tag, recent_dms ); } recent_dms.put( dm, now ); return( true ); } private boolean testConstraint( DownloadManager dm ) { List<Tag> dm_tags = tag_manager.getTagsForTaggable( dm ); return( expr.eval( dm, dm_tags )); } } private interface ConstraintExpr { public boolean eval( DownloadManager dm, List<Tag> tags ); public String getString(); } private class ConstraintExprTrue implements ConstraintExpr { public boolean eval( DownloadManager dm, List<Tag> tags ) { return( true ); } public String getString() { return( "true" ); } } private class ConstraintExprParams implements ConstraintExpr { private String value; private ConstraintExprParams( String _value ) { value = _value.trim(); } public boolean eval( DownloadManager dm, List<Tag> tags ) { return( false ); } public Object[] getValues() { if ( value.length() == 0 ){ return( new String[0]); }else if ( !value.contains( "," )){ return( new Object[]{ value }); }else{ char[] chars = value.toCharArray(); boolean in_quote = false; List<String> params = new ArrayList<String>(16); StringBuffer current_param = new StringBuffer( value.length()); for (int i=0;i<chars.length;i++){ char c = chars[i]; if ( c == '"' ){ if ( i == 0 || chars[i-1] != '\\' ){ in_quote = !in_quote; } } if ( c == ',' && !in_quote ){ params.add( current_param.toString()); current_param.setLength( 0 ); }else{ if ( in_quote || !Character.isWhitespace( c )){ current_param.append( c ); } } } params.add( current_param.toString()); return( params.toArray( new Object[ params.size()])); } } public String getString() { return( value ); } } private class ConstraintExprNot implements ConstraintExpr { private ConstraintExpr expr; private ConstraintExprNot( ConstraintExpr e ) { expr = e; } public boolean eval( DownloadManager dm, List<Tag> tags ) { return( !expr.eval( dm, tags )); } public String getString() { return( "!(" + expr.getString() + ")"); } } private class ConstraintExprOr implements ConstraintExpr { private ConstraintExpr[] exprs; private ConstraintExprOr( ConstraintExpr[] _exprs ) { exprs = _exprs; } public boolean eval( DownloadManager dm, List<Tag> tags ) { for ( ConstraintExpr expr: exprs ){ if ( expr.eval( dm, tags )){ return( true ); } } return( false ); } public String getString() { String res = ""; for ( int i=0;i<exprs.length;i++){ res += (i==0?"":"||") + exprs[i].getString(); } return( "(" + res + ")" ); } } private class ConstraintExprAnd implements ConstraintExpr { private ConstraintExpr[] exprs; private ConstraintExprAnd( ConstraintExpr[] _exprs ) { exprs = _exprs; } public boolean eval( DownloadManager dm, List<Tag> tags ) { for ( ConstraintExpr expr: exprs ){ if ( !expr.eval( dm, tags )){ return( false ); } } return( true ); } public String getString() { String res = ""; for ( int i=0;i<exprs.length;i++){ res += (i==0?"":"&&") + exprs[i].getString(); } return( "(" + res + ")" ); } } private class ConstraintExprXor implements ConstraintExpr { private ConstraintExpr[] exprs; private ConstraintExprXor( ConstraintExpr[] _exprs ) { exprs = _exprs; if ( exprs.length < 2 ){ throw( new RuntimeException( "Two or more arguments required for ^" )); } } public boolean eval( DownloadManager dm, List<Tag> tags ) { boolean res = exprs[0].eval( dm, tags ); for ( int i=1;i<exprs.length;i++){ res = res ^ exprs[i].eval( dm, tags ); } return( res ); } public String getString() { String res = ""; for ( int i=0;i<exprs.length;i++){ res += (i==0?"":"^") + exprs[i].getString(); } return( "(" + res + ")" ); } } private static final int FT_HAS_TAG = 1; private static final int FT_IS_PRIVATE = 2; private static final int FT_GE = 3; private static final int FT_GT = 4; private static final int FT_LE = 5; private static final int FT_LT = 6; private static final int FT_EQ = 7; private static final int FT_NEQ = 8; private static final int FT_CONTAINS = 9; private static final int FT_MATCHES = 10; private class ConstraintExprFunction implements ConstraintExpr { private final String func_name; private final ConstraintExprParams params_expr; private final Object[] params; private final int fn_type; private ConstraintExprFunction( String _func_name, ConstraintExprParams _params ) { func_name = _func_name; params_expr = _params; params = _params.getValues(); boolean params_ok = false; if ( func_name.equals( "hasTag" )){ fn_type = FT_HAS_TAG; params_ok = params.length == 1 && getStringLiteral( params, 0 ); }else if ( func_name.equals( "isPrivate" )){ fn_type = FT_IS_PRIVATE; params_ok = params.length == 0; }else if ( func_name.equals( "isGE" )){ fn_type = FT_GE; params_ok = params.length == 2; }else if ( func_name.equals( "isGT" )){ fn_type = FT_GT; params_ok = params.length == 2; }else if ( func_name.equals( "isLE" )){ fn_type = FT_LE; params_ok = params.length == 2; }else if ( func_name.equals( "isLT" )){ fn_type = FT_LT; params_ok = params.length == 2; }else if ( func_name.equals( "isEQ" )){ fn_type = FT_EQ; params_ok = params.length == 2; }else if ( func_name.equals( "isNEQ" )){ fn_type = FT_NEQ; params_ok = params.length == 2; }else if ( func_name.equals( "contains" )){ fn_type = FT_CONTAINS; params_ok = params.length == 2; }else if ( func_name.equals( "matches" )){ fn_type = FT_MATCHES; params_ok = params.length == 2 && getStringLiteral( params, 1 ); }else{ throw( new RuntimeException( "Unsupported function '" + func_name + "'" )); } if ( !params_ok ){ throw( new RuntimeException( "Invalid parameters for function '" + func_name + "': " + params_expr.getString())); } } public boolean eval( DownloadManager dm, List<Tag> tags ) { switch( fn_type ){ case FT_HAS_TAG:{ String tag_name = (String)params[0]; for ( Tag t: tags ){ if ( t.getTagName( true ).equals( tag_name )){ return( true ); } } break; } case FT_IS_PRIVATE:{ TOTorrent t = dm.getTorrent(); return( t != null && t.getPrivate()); } case FT_GE: case FT_GT: case FT_LE: case FT_LT: case FT_EQ: case FT_NEQ:{ Number n1 = getNumeric( dm, params, 0 ); Number n2 = getNumeric( dm, params, 1 ); switch( fn_type ){ case FT_GE: return( n1.doubleValue() >= n2.doubleValue()); case FT_GT: return( n1.doubleValue() > n2.doubleValue()); case FT_LE: return( n1.doubleValue() <= n2.doubleValue()); case FT_LT: return( n1.doubleValue() < n2.doubleValue()); case FT_EQ: return( n1.doubleValue() == n2.doubleValue()); case FT_NEQ: return( n1.doubleValue() != n2.doubleValue()); } return( false ); } case FT_CONTAINS:{ String s1 = getString( dm, params, 0 ); String s2 = getString( dm, params, 1 ); return( s1.contains( s2 )); } case FT_MATCHES:{ String s1 = getString( dm, params, 0 ); if ( params[1] == null ){ return( false ); }else if ( params[1] instanceof Pattern ){ return(((Pattern)params[1]).matcher( s1 ).find()); }else{ try{ Pattern p = Pattern.compile((String)params[1], Pattern.CASE_INSENSITIVE ); params[1] = p; return( p.matcher( s1 ).find()); }catch( Throwable e ){ Debug.out( "Invalid constraint pattern: " + params[1] ); params[1] = null; } } return( false ); } } return( false ); } private boolean getStringLiteral( Object[] args, int index ) { Object _arg = args[index]; if ( _arg instanceof String ){ String arg = (String)_arg; if ( arg.startsWith( "\"" ) && arg.endsWith( "\"" )){ args[index] = arg.substring( 1, arg.length() - 1 ); return( true ); } } return( false ); } private String getString( DownloadManager dm, Object[] args, int index ) { String str = (String)args[index]; if ( str.startsWith( "\"" ) && str.endsWith( "\"" )){ return( str.substring( 1, str.length() - 1 )); }else if ( str.equals( "name" )){ return( dm.getDisplayName()); }else{ Debug.out( "Invalid constraint string: " + str ); String result = "\"\""; args[index] = result; return( result ); } } private Number getNumeric( DownloadManager dm, Object[] args, int index ) { Object arg = args[index]; if ( arg instanceof Number ){ return((Number)arg); } String str = (String)arg; Number result = 0; try{ if ( Character.isDigit( str.charAt(0))){ if ( str.contains( "." )){ result = Float.parseFloat( str ); }else{ result = Long.parseLong( str ); } return( result ); }else if ( str.equals( "shareratio" )){ result = null; // don't cache this! int sr = dm.getStats().getShareRatio(); if ( sr == -1 ){ return( Integer.MAX_VALUE ); }else{ return( new Float( dm.getStats().getShareRatio()/1000.0f )); } }else{ Debug.out( "Invalid constraint numeric: " + str ); return( result ); } }catch( Throwable e){ Debug.out( "Invalid constraint numeric: " + str ); return( result ); }finally{ if ( result != null ){ // cache literal results args[index] = result; } } } public String getString() { return( func_name + "(" + params_expr.getString() + ")" ); } } public static void main( String[] args ) { TagPropertyConstraintHandler handler = new TagPropertyConstraintHandler(); //System.out.println( handler.compileConstraint( "!(hasTag(\"bil\") && (hasTag( \"fred\" ))) || hasTag(\"toot\")" ).getString()); System.out.println( handler.compileConstraint( "isGE( shareratio, 1.5)" ).getString()); } }