001/* 002 * Copyright (c) 2007-2016 Concurrent, 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 }