    001/*
    002 * Copyright (c) 2007-2017 Xplenty, Inc. All Rights Reserved.
    003 *
    004 * Project and contact information: http://www.cascading.org/
    005 *
    006 * This file is part of the Cascading project.
    007 *
    008 * Licensed under the Apache License, Version 2.0 (the "License");
    009 * you may not use this file except in compliance with the License.
    010 * You may obtain a copy of the License at
    011 *
    012 *     http://www.apache.org/licenses/LICENSE-2.0
    013 *
    014 * Unless required by applicable law or agreed to in writing, software
    015 * distributed under the License is distributed on an "AS IS" BASIS,
    016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    017 * See the License for the specific language governing permissions and
    018 * limitations under the License.
    019 */
    020
    021package cascading.util;
    022
    023import java.io.IOException;
    024import java.io.InputStream;
    025import java.io.UnsupportedEncodingException;
    026import java.net.InetAddress;
    027import java.net.MalformedURLException;
    028import java.net.URL;
    029import java.net.URLConnection;
    030import java.net.URLEncoder;
    031import java.util.Collections;
    032import java.util.Properties;
    033import java.util.Set;
    034import java.util.Timer;
    035import java.util.TimerTask;
    036import java.util.TreeSet;
    037import java.util.concurrent.atomic.AtomicInteger;
    038
    039import cascading.flow.planner.FlowPlanner;
    040import cascading.flow.planner.PlatformInfo;
    041import cascading.property.AppProps;
    042import org.slf4j.Logger;
    043import org.slf4j.LoggerFactory;
    044
    045/**
    046 *
    047 */
    048public class Update extends TimerTask
    049  {
    050  private static final Logger LOG = LoggerFactory.getLogger( Update.class );
    051  private static final String UPDATE_PROPERTIES = "latest.properties";
    052
    053  public static final String UPDATE_CHECK_SKIP = "cascading.update.skip";
    054  public static final String UPDATE_URL = "cascading.update.url";
    055
    056  private static final Set<String> plannerSet = Collections.synchronizedSet( new TreeSet<String>() );
    057  private static final Set<PlatformInfo> platformInfoSet = Collections.synchronizedSet( new TreeSet<PlatformInfo>() );
    058  private static Timer timer;
    059  private static AtomicInteger requests = new AtomicInteger( 0 ); // successful requests
    060  private static AtomicInteger pending = new AtomicInteger( 0 ); // pending requests
    061
    062  // set foundation for checking planner versions from third-parties
    063  public static void registerPlanner( Class<? extends FlowPlanner> plannerClass )
    064    {
    065    if( Boolean.getBoolean( UPDATE_CHECK_SKIP ) || plannerClass == null )
    066      return;
    067
    068    if( !plannerSet.add( plannerClass.getSimpleName() ) ) // not new
    069      return;
    070
    071    if( timer == null || pending.get() > 0 ) // already have updates queued, do not schedule
    072      return;
    073
    074    timer.schedule( new Update(), 1000 * 30 );
    075    }
    076
    077  public static synchronized void checkForUpdate( PlatformInfo platformInfo )
    078    {
    079    if( Boolean.getBoolean( UPDATE_CHECK_SKIP ) )
    080      return;
    081
    082    if( platformInfo != null )
    083      platformInfoSet.add( platformInfo );
    084
    085    if( timer != null )
    086      return;
    087
    088    timer = new Timer( "UpdateRequestTimer", true );
    089    timer.scheduleAtFixedRate( new Update(), 1000 * 30, 24 * 60 * 60 * 1000L );
    090    }
    091
    092  private boolean hasUpdated = false;
    093
    094  public Update()
    095    {
    096    pending.incrementAndGet(); // update queued
    097    }
    098
    099  @Override
    100  public void run()
    101    {
    102    checkForUpdate();
    103
    104    if( hasUpdated ) // don't count subsequent updates from this instance
    105      return;
    106
    107    hasUpdated = true;
    108    pending.decrementAndGet(); // update completed
    109    }
    110
    111  public boolean checkForUpdate()
    112    {
    113    if( !Version.hasMajorMinorVersionInfo() )
    114      return true;
    115
    116    boolean isCurrentWip = Version.getReleaseFull() != null && Version.getReleaseFull().contains( "wip" );
    117    boolean isCurrentDev = Version.getReleaseFull() == null || Version.getReleaseFull().contains( "wip-dev" );
    118
    119    URL updateCheckUrl = getUpdateCheckUrl();
    120
    121    if( updateCheckUrl == null )
    122      return false;
    123
    124    // do this before fetching latest.properties
    125    if( isCurrentDev )
    126      {
    127      LOG.debug( "current release is dev build, update url: {}", updateCheckUrl.toString() );
    128      return true;
    129      }
    130
    131    Properties latestProperties = getUpdateProperties( updateCheckUrl );
    132
    133    if( latestProperties.isEmpty() )
    134      return false;
    135
    136    String latestMajor = latestProperties.getProperty( Version.CASCADING_RELEASE_MAJOR );
    137    String latestMinor = latestProperties.getProperty( Version.CASCADING_RELEASE_MINOR );
    138
    139    boolean isSameMajorRelease = equals( Version.getReleaseMajor(), latestMajor );
    140    boolean isSameMinorRelease = equals( Version.getReleaseMinor(), latestMinor );
    141
    142    if( isSameMajorRelease && isSameMinorRelease )
    143      {
    144      LOG.debug( "no updates available" );
    145      return true;
    146      }
    147
    148    String version = latestProperties.getProperty( "cascading.release.version" );
    149
    150    if( version == null )
    151      LOG.debug( "release version info not found" );
    152    else
    153      LOG.info( "newer Cascading release available: {}", version );
    154
    155    return true;
    156    }
    157
    158  private static Properties getUpdateProperties( URL updateUrl )
    159    {
    160    try
    161      {
    162      URLConnection connection = updateUrl.openConnection();
    163      connection.setConnectTimeout( 3 * 1000 );
    164
    165      InputStream inputStream = connection.getInputStream();
    166
    167      try
    168        {
    169        Properties props = new Properties();
    170
    171        if( inputStream != null )
    172          {
    173          props.load( inputStream );
    174          requests.incrementAndGet();
    175          }
    176
    177        return props;
    178        }
    179      finally
    180        {
    181        close( inputStream );
    182        }
    183      }
    184    catch( IOException exception )
    185      {
    186      // do not reschedule requests
    187      LOG.debug( "unable to fetch latest properties", exception );
    188      return new Properties();
    189      }
    190    }
    191
    192  private static URL getUpdateCheckUrl()
    193    {
    194    String url = buildURL();
    195
    196    String connector = url.indexOf( '?' ) > 0 ? "&" : "?";
    197
    198    String spec = url + connector + buildParamsString();
    199
    200    try
    201      {
    202      return new URL( spec );
    203      }
    204    catch( MalformedURLException exception )
    205      {
    206      LOG.debug( "malformed url: {}", spec, exception );
    207      return null;
    208      }
    209    }
    210
    211  private static String buildURL()
    212    {
    213    String baseURL = System.getProperty( UPDATE_URL, "" );
    214
    215    if( baseURL.isEmpty() )
    216      {
    217      String releaseBuild = Version.getReleaseBuild();
    218
    219      // if wip, only test if a newer wip version is available
    220      if( releaseBuild != null && releaseBuild.contains( "wip" ) )
    221        baseURL = "http://files.concurrentinc.com/cascading/";
    222      else
    223        baseURL = "http://files.cascading.org/cascading/";
    224      }
    225
    226    if( !baseURL.endsWith( "/" ) )
    227      baseURL += "/";
    228
    229    baseURL = String.format( "%s%s/%s", baseURL, Version.getReleaseMajor(), UPDATE_PROPERTIES );
    230
    231    return baseURL;
    232    }
    233
    234  private static String buildParamsString()
    235    {
    236    StringBuilder sb = new StringBuilder();
    237
    238    sb.append( "id=" );
    239    sb.append( getClientId() );
    240    sb.append( "&instance=" );
    241    sb.append( urlEncode( AppProps.getApplicationID() ) );
    242    sb.append( "&request=" );
    243    sb.append( requests.get() );
    244    sb.append( "&os-name=" );
    245    sb.append( urlEncode( getProperty( "os.name" ) ) );
    246    sb.append( "&jvm-name=" );
    247    sb.append( urlEncode( getProperty( "java.vm.name" ) ) );
    248    sb.append( "&jvm-version=" );
    249    sb.append( urlEncode( getProperty( "java.version" ) ) );
    250    sb.append( "&os-arch=" );
    251    sb.append( urlEncode( getProperty( "os.arch" ) ) );
    252    sb.append( "&product=" );
    253    sb.append( urlEncode( Version.CASCADING ) );
    254    sb.append( "&version=" );
    255    sb.append( urlEncode( Version.getReleaseFull() ) );
    256    sb.append( "&version-build=" );
    257    sb.append( urlEncode( Version.getReleaseBuild() ) );
    258    sb.append( "&frameworks=" );
    259    sb.append( urlEncode( getProperty( AppProps.APP_FRAMEWORKS ) ) );
    260
    261    if( getProperty( "driven.agent.name" ) != null )
    262      {
    263      sb.append( "&agent-name=" );
    264      sb.append( urlEncode( getProperty( "driven.agent.name" ) ) );
    265      sb.append( "&agent-version=" );
    266      sb.append( urlEncode( getProperty( "driven.agent.version" ) ) );
    267      }
    268
    269    synchronized( plannerSet )
    270      {
    271      for( String plannerName : plannerSet )
    272        {
    273        sb.append( "&planner-name=" );
    274        sb.append( urlEncode( plannerName ) );
    275        }
    276      }
    277
    278    synchronized( platformInfoSet )
    279      {
    280      for( PlatformInfo platformInfo : platformInfoSet )
    281        {
    282        sb.append( "&platform-name=" );
    283        sb.append( urlEncode( platformInfo.name ) );
    284        sb.append( "&platform-version=" );
    285        sb.append( urlEncode( platformInfo.version ) );
    286        sb.append( "&platform-vendor=" );
    287        sb.append( urlEncode( platformInfo.vendor ) );
    288        }
    289      }
    290
    291    return sb.toString();
    292    }
    293
    294  private static boolean equals( String lhs, String rhs )
    295    {
    296    return lhs != null && lhs.equals( rhs );
    297    }
    298
    299  private static int getClientId()
    300    {
    301    try
    302      {
    303      return Math.abs( InetAddress.getLocalHost().hashCode() );
    304      }
    305    catch( Throwable t )
    306      {
    307      return 0;
    308      }
    309    }
    310
    311  private static String urlEncode( String param )
    312    {
    313    if( param == null )
    314      return "";
    315
    316    try
    317      {
    318      return URLEncoder.encode( param, "UTF-8" );
    319      }
    320    catch( UnsupportedEncodingException exception )
    321      {
    322      LOG.debug( "unable to encode param: {}", param, exception );
    323
    324      return null;
    325      }
    326    }
    327
    328  private static String getProperty( String prop )
    329    {
    330    return System.getProperty( prop, "" );
    331    }
    332
    333  private static void close( InputStream in )
    334    {
    335    try
    336      {
    337      if( in != null )
    338        in.close();
    339      }
    340    catch( IOException exception )
    341      {
    342      // do nothing
    343      }
    344    }
    345  }