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  }