001/*license*\
002   Codelet: Copyright (C) 2014, Jeff Epstein (aliteralmind __DASH__ github __AT__ yahoo __DOT__ com)
003
004   This software is dual-licensed under the:
005   - Lesser General Public License (LGPL) version 3.0 or, at your option, any later version;
006   - Apache Software License (ASL) version 2.0.
007
008   Either license may be applied at your discretion. More information may be found at
009   - http://en.wikipedia.org/wiki/Multi-licensing.
010
011   The text of both licenses is available in the root directory of this project, under the names "LICENSE_lgpl-3.0.txt" and "LICENSE_asl-2.0.txt". The latest copies may be downloaded at:
012   - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
013   - ASL 2.0: http://www.apache.org/licenses/LICENSE-2.0.txt
014\*license*/
015package  com.github.aliteralmind.codelet.util;
016   import  com.github.xbn.array.Duplicates;
017   import  com.github.xbn.array.NullContainer;
018   import  com.github.xbn.array.NullElement;
019   import  com.github.xbn.io.IOUtil;
020   import  com.github.xbn.io.NewPrintWriterToFile;
021   import  com.github.xbn.io.NewTextAppenterFor;
022   import  com.github.xbn.io.PlainTextFileUtil;
023   import  com.github.xbn.io.RTIOException;
024   import  com.github.xbn.io.TextAppenter;
025   import  com.github.xbn.keyed.SimpleNamed;
026   import  com.github.xbn.lang.CrashIfObject;
027   import  com.github.xbn.list.CrashIfList;
028   import  com.github.xbn.text.CrashIfString;
029   import  com.github.xbn.text.StringUtil;
030   import  com.github.xbn.util.IfError;
031   import  com.github.xbn.util.JavaRegexes;
032   import  java.io.PrintWriter;
033   import  java.util.ArrayList;
034   import  java.util.Collections;
035   import  java.util.Iterator;
036   import  java.util.List;
037   import  java.util.regex.Matcher;
038   import  java.util.regex.Pattern;
039/**
040   <p>Represents the <a href="http://docs.oracle.com/javase/1.5.0/docs/tooldocs/windows/javadoc.html#linkpackagelist">{@code package-list}</a> for a single external Java library, including a duplicate offline file that is automatically updated from it.</p>
041
042 * @since  0.1.0
043 * @author  Copyright (C) 2014, Jeff Epstein ({@code aliteralmind __DASH__ github __AT__ yahoo __DOT__ com}), dual-licensed under the LGPL (version 3.0 or later) or the ASL (version 2.0). See source code for details. <a href="http://codelet.aliteralmind.com">{@code http://codelet.aliteralmind.com}</a>, <a href="https://github.com/aliteralmind/codelet">{@code https://github.com/aliteralmind/codelet}</a>
044 **/
045public class OnlineOfflineDocRoot extends SimpleNamed  {
046   private final String urlDir;
047   private final String path;
048   private final List<String> pkgList;
049   /**
050      <p>Create a new instance from a url, offline path, and package list.</p>
051
052    * @param  name  Descriptive name of the external library. May not be {@code null} or empty, and must contain only letters, digits, and underscores. Get with {@link com.github.xbn.keyed.SimpleNamed#getName() getName}{@code ()}*
053    * @param  url_toDocRoot  The url directory in which the <a href="http://docs.oracle.com/javase/1.5.0/docs/tooldocs/windows/javadoc.html#linkpackagelist">{@code package-list}</a> file exists. <i>Should</i> be a valid url and end with a slash ({@code '/'}). Get with {@link #getUrlDir() getUrlDir}{@code ()}.
054    * @param  offline_path  The full path of the offline duplicate of the {@code package-list} file. <i>Should</i> be a valid represent a text file that is both readable and writable. Get with {@link #getPath() getPath}{@code ()}
055    * @param  package_list  The list of packages in the external library, as found in {@code package-list}. May not be {@code null}, empty, and its elements may not be {@code null}, empty, or duplicate. Get (a duplicate of this list) with {@link #getPackageList() getPackageList}{@code ()}.
056    * @see  #newFromOnline(String, String, String, int, long, IfError, RefreshOffline, Appendable, Appendable) newFromOnline
057    * @see  #newFromOffline(String, String, String, Appendable, Appendable) newFromOffline
058    */
059   public OnlineOfflineDocRoot(String name, String url_toDocRoot, String offline_path, List<String> package_list)  {
060      this(false, name, url_toDocRoot, offline_path, package_list);
061      CrashIfList.bad(package_list, "package_list", NullContainer.BAD, 1, null, NullElement.BAD, 1, null, Duplicates.BAD);
062   }
063      /**
064         <p>Avoids calling CrashIfList.bad when this OnlineOfflineDocRoot is internally created.</p>
065       */
066      private OnlineOfflineDocRoot(boolean ignored, String name, String url_toDocRoot, String offline_path, List<String> package_list)  {
067         super(name);
068         CrashIfString.nullEmpty(url_toDocRoot, "url_toDocRoot", null);
069         CrashIfString.nullEmpty(offline_path, "offline_path", null);
070         urlDir = url_toDocRoot;
071         path = offline_path;
072         try  {
073            pkgList = Collections.<String>unmodifiableList(package_list);
074         }  catch(RuntimeException rx)  {
075            throw  CrashIfObject.nullOrReturnCause(package_list, "package_list", null, rx);
076         }
077      }
078   /**
079      <p>Url of the directory in which the {@code package-list} file exists.</p>
080
081    * @return  A non-{@code null} url, ending with a slash ({@code '/'}).
082    * @see  #getPath()
083    */
084   public String getUrlDir()  {
085      return  urlDir;
086   }
087   /**
088      <p>Full path to the offline duplicate of the {@code package-list} file.</p>
089
090    * @return  A non-{@code null} path, including the file-name.
091    * @see  #getUrlDir()
092    */
093   public String getPath()  {
094      return  path;
095   }
096   /**
097      <p>An immutable list of all package names.</p>
098    */
099   public List<String> getPackageList()  {
100      return  pkgList;
101   }
102   /**
103      <p>Overwrites or creates offline file with the current package list.</p>
104
105      <p>This overwrites the current {@linkplain #getPath() offline file} with the contents of the {@linkplain #getPackageList() package list}.</p>
106
107    * @param  debug_ifNonNull  If non-{@code null}, the destination for progress debugging.
108    * @param  dbgError_ifNonNull  If non-{@code null}, the destination for the error debugging. If {@code if_error.WARN}, this parameter may not be {@code null}.
109    * @exception  RTFileNotFoundException  If a {@link java.io.FileNotFoundException FileNotFoundException} is thrown when trying to open the file. The original exception (for this and all exceptions thrown by this function) is accessible with {@link java.lang.Throwable#getCause() getCause}{@code ()}.
110    * @exception  RTIOException  If an {@link java.io.IOException IOException} is thrown when trying to open the file.
111    * @exception  SecurityException  If the file is not writable.
112    * @exception  RuntimeException  If the file is successfully opened, but cannot be written to.
113    * @see  #getPackageList()
114    */
115   public void refreshOffline(Appendable debug_ifNonNull, Appendable dbgError_ifNonNull)  {
116      PrintWriter pw = null;
117
118      if(debug_ifNonNull != null)  {
119         NewTextAppenterFor.appendable(debug_ifNonNull).
120            appentln("refreshOffline:" + toString());
121      }
122
123      pw = new NewPrintWriterToFile().overwrite().autoFlush().build(getPath());
124
125      for(String pkg : getPackageList())  {
126         try  {
127            pw.println(pkg);
128         }  catch(Exception x)  {
129            throw  new RuntimeException("getPath()=\"" + getPath() + "\"", x);
130         }
131      }
132   }
133   /**
134    * @return  <code>{@link #appendToString(StringBuilder) appendToString}(new StringBuilder()).toString()</code>
135    */
136   public String toString()  {
137      return  appendToString(new StringBuilder()).toString();
138   }
139   /**
140    * @param  to_appendTo May not be {@code null}.
141    * @see  #toString()
142    */
143   public StringBuilder appendToString(StringBuilder to_appendTo)  {
144      try  {
145         to_appendTo.append(getName()).append("(").
146            append(getPackageList().size()).append(" package");
147      }  catch(RuntimeException rx)  {
148         throw  CrashIfObject.nullOrReturnCause(to_appendTo, "to_appendTo", null, rx);
149      }
150      if(getPackageList().size() > 1)  {
151         to_appendTo.append("s");
152      }
153      to_appendTo.append("): ").append(getUrlDir());
154
155      return  to_appendTo;
156   }
157   /**
158      <p>Create a new instance from an online {@code package-list}.</p>
159
160    * @param  refresh_offline  If {@link com.github.aliteralmind.codelet.util.RefreshOffline#YES YES}, then this ends by calling
161      <br/> &nbsp; &nbsp; <code><i>[the-new-OnlineOfflineDocRoot]</i>.{@link #refreshOffline(Appendable, Appendable) refreshOffline}(debug_ifNonNull, dbgError_ifNonNull)</code>
162      <br/>This parameter may not be {@code null}.
163    * @return  <code>new {@link #OnlineOfflineDocRoot(String, String, String, List) OnlineOfflineDocRoot}(url_toDocRoot, offline_path,
164      <br/> &nbsp; &nbsp; {@link #newPackageListFromOnline(String, int, long, IfError, Appendable, Appendable) newPackageListFromOnline}(offline_path,
165      <br/> &nbsp; &nbsp; &nbsp; &nbsp; error_attemptCount, error_sleepMills, if_error, debug_ifNonNull, dbgError_ifNonNull))</code>
166      <br/>If the package list cannot be retrieved, and {@code if_error.}{@code com.github.xbn.util.IfError#WARN WARN}, this returns {@code null}.
167      <br/> &nbsp; &nbsp;
168    * @see  #newFromOffline(String, String, String, Appendable, Appendable) newFromOffline
169    */
170   public static final OnlineOfflineDocRoot newFromOnline(String name, String url_toDocRoot, String offline_path, int error_attemptCount, long error_sleepMills, IfError if_error, RefreshOffline refresh_offline, Appendable debug_ifNonNull, Appendable dbgError_ifNonNull)  throws InterruptedException  {
171      List<String> pkgList = newPackageListFromOnline(url_toDocRoot,
172         error_attemptCount, error_sleepMills, if_error, debug_ifNonNull, dbgError_ifNonNull);
173      if(pkgList == null)  {
174         return  null;
175      }
176      OnlineOfflineDocRoot docRoot = new OnlineOfflineDocRoot(false, name,
177         url_toDocRoot, offline_path, pkgList);
178      try  {
179         if(refresh_offline.isYes())  {
180            docRoot.refreshOffline(debug_ifNonNull, dbgError_ifNonNull);
181         }
182      }  catch(RuntimeException rx)  {
183         throw  CrashIfObject.nullOrReturnCause(refresh_offline, "refresh_offline", null, rx);
184      }
185      return  docRoot;
186   }
187   /**
188      <p>Create a new instance from an offline {@code package-list}.</p>
189
190    * @return  <code>new {@link #OnlineOfflineDocRoot(String, String, String, List) OnlineOfflineDocRoot}(url_toDocRoot, offline_path,
191      <br/> &nbsp; &nbsp; {@link #newPackageListFromOffline(String, Appendable, Appendable) newPackageListFromOnline}(offline_path, if_error, debug_ifNonNull, dbgError_ifNonNull))</code>
192    * @see  #newFromOnline(String, String, String, int, long, IfError, RefreshOffline, Appendable, Appendable) newFromOnline
193    */
194   public static final OnlineOfflineDocRoot newFromOffline(String name, String url_toDocRoot, String offline_path, Appendable debug_ifNonNull, Appendable dbgError_ifNonNull)  {
195      return  new OnlineOfflineDocRoot(false, name, url_toDocRoot, offline_path,
196         newPackageListFromOffline(offline_path, debug_ifNonNull, dbgError_ifNonNull));
197   }
198   /**
199      <p>Create a package list from an offline {@code package-list} file.</p>
200
201    * @param  pkgList_path  The full path to the local package-list file. Must represent a file that is  readable and writable (writability is not verified until the offline file is {@link #refreshOffline(Appendable, Appendable) refreshed}), and a valid <a href="http://docs.oracle.com/javase/1.5.0/docs/tooldocs/windows/javadoc.html#linkpackagelist">{@code package-list}</a>.
202    * @param  debug_ifNonNull  If non-{@code null}, the destination for progress debugging.
203    * @param  dbgError_ifNonNull  If non-{@code null}, the destination for the error debugging.
204    * @exception  RuntimeException  If opening or reading the file fails. The original exception is accessible with {@link java.lang.Throwable#getCause() getCause}{@code ()}.
205    * @see  #newFromOffline(String, String, String, Appendable, Appendable) newFromOffline
206    * @see  #newPackageListFromOnline(String, int, long, IfError, Appendable, Appendable) newPackageListFromOnline
207    */
208   public static final List<String> newPackageListFromOffline(String pkgList_path, Appendable debug_ifNonNull, Appendable dbgError_ifNonNull)  {
209      if(debug_ifNonNull != null)  {
210         NewTextAppenterFor.appendable(debug_ifNonNull).
211            appentln("newPackageListFromOffline:\"" + pkgList_path + "\"");
212      }
213      Iterator<String> lineItr = null;
214      try  {
215         lineItr = PlainTextFileUtil.getLineIterator(pkgList_path, "pkgList_path");
216      }  catch(Exception x)  {
217         String msg = "Unable to retrieve package-list from pkgList_path=\"" + pkgList_path + "\"";
218         NewTextAppenterFor.appendableSuppressIfNull(dbgError_ifNonNull).
219            appentln(msg + ": " + x);
220
221         throw  new RuntimeException(msg, x);
222      }
223      try  {
224         return  newPkgListFromLineItr(lineItr);
225      }  catch(PackageListRetrievalFailedException plfx)  {
226         String msg = "pkgList_path=\"" + pkgList_path + "\"";
227         NewTextAppenterFor.appendableSuppressIfNull(dbgError_ifNonNull).
228            appentln(msg + ": " + plfx);
229         throw  new PackageListRetrievalFailedException(msg, plfx);
230      }
231   }
232   /**
233      <p>Create a package list from an online {@code package-list}.</p>
234
235    * @param  url_toDocRoot  The url directory in which a valid <a href="http://docs.oracle.com/javase/7/docs/technotes/tools/solaris/javadoc.html#package-list">{@code package-list}</a> file exists. Must end with a slash ({@code '/'}).
236    * @param  error_attemptCount  The number of attempts to make when retrieving the {@code package-list} fails. May not be less than one.
237    * @param  error_sleepMills  The number of milliseconds to {@linkplain java.lang.Thread#sleep(long) sleep} between each attempt. May not be less than zero.
238    * @param  if_error  If {@link com.github.xbn.util.IfError#WARN WARN} then, after all attempts fail, the error is logged to {@code dbgError_ifNonNull}. If {@link com.github.xbn.util.IfError#CRASH CRASH} a {@code RuntimeException} is also thrown. This parameter may not be {@code null}.
239    * @param  dbgError_ifNonNull  The destination for the error-warning message. If {@code if_error.WARN}, may not be {@code null}.
240    * @return  If the {@code package-list}<ul>
241         <li>Was successfully retrieved: A non-{@code null} list containing all its packages.</li>
242         <li>Could not be retrieved and {@code if_error.WARN}: {@code null}</li>
243      </ul>
244    * @exception  RuntimeException  If any error occurs when retrieving the {@code package-list}. The causing error is accessible with {@link java.lang.Throwable#getCause() getCause}{@code ()}.
245    * @exception  RTIOException  If using {@code dbgError_ifNonNull} fails.
246    * @see  #newFromOnline(String, String, String, int, long, IfError, RefreshOffline, Appendable, Appendable) newFromOnline
247    * @see  #newPackageListFromOffline(String, Appendable, Appendable) newPackageListFromOffline
248    */
249   public static final List<String> newPackageListFromOnline(String url_toDocRoot, int error_attemptCount, long error_sleepMills, IfError if_error, Appendable debug_ifNonNull, Appendable dbgError_ifNonNull)  throws InterruptedException  {
250      TextAppenter dbgAptr = NewTextAppenterFor.appendableUnusableIfNull(debug_ifNonNull);
251      TextAppenter dbgErrAptr = null;
252      try  {
253         dbgErrAptr = if_error.newAptrForApblCrashIfWarn(dbgError_ifNonNull, "dbgError_ifNonNull");
254      }  catch(RuntimeException rx)  {
255         throw  CrashIfObject.nullOrReturnCause(if_error, "if_error", null, rx);
256      }
257
258      if(dbgAptr.isUseable())  {
259         dbgAptr.appentln("Loading package-list from url(if_error=" + if_error + "): \"" + url_toDocRoot + "\"");
260      }
261      String text = null;
262      String errorMsg = null;
263      Exception x = null;
264      while(error_attemptCount > 0)  {
265         try  {
266            text = IOUtil.getWebPageSourceX(url_toDocRoot + "package-list", null);
267
268            if(text != null)  {
269               return  newPkgListFromLineItr(StringUtil.getLineIterator(text));
270            }
271         }  catch(Exception x2)  {
272            errorMsg = "Error loading Codelet config: Failure loading package list from url: \"" + url_toDocRoot + "\"";
273            x = x2;
274            dbgErrAptr.appentln(errorMsg + ": " + x2);
275         }
276
277         error_attemptCount--;
278
279         if(error_attemptCount > 0)  {
280            dbgErrAptr.appentln("newPackageListFromOnline(if_error=" + if_error + "):\"" + url_toDocRoot + "\"");
281            dbgErrAptr.appentln("Pausing " + error_sleepMills + ". Attempts remaining=" + error_attemptCount);
282            Thread.sleep(error_sleepMills);
283         }
284      }
285
286      //If if_error is null, that will have been caught by printWarning_crashIfApblNull
287      if(if_error.isCrash())  {
288         throw  new PackageListRetrievalFailedException(errorMsg, x);
289      }
290      return  null;
291   }
292      private static final List<String> newPkgListFromLineItr(Iterator<String> line_itr)  {
293         List<String> pkgList = new ArrayList<String>(10);
294         int lineNum = 1;
295         while(line_itr.hasNext())  {
296            String pkg = line_itr.next();
297            if(!pkgMtchr.reset(pkg).matches())  {
298               throw  new IllegalArgumentException("Line " + lineNum + " is not a valid package: \"" + pkg + "\"");
299            }
300            pkgList.add(pkg);
301            lineNum++;
302         }
303         Collections.sort(pkgList);
304         return  pkgList;
305      }
306      private static final Matcher pkgMtchr = Pattern.compile(JavaRegexes.PACKAGE_NAME).matcher("");
307}