/*
* 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());
}
}