| /*=============================================================================# |
| # Copyright (c) 2010, 2020 Stephan Wahlbrink and others. |
| # |
| # This program and the accompanying materials are made available under the |
| # terms of the Eclipse Public License 2.0 which is available at |
| # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 |
| # which is available at https://www.apache.org/licenses/LICENSE-2.0. |
| # |
| # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 |
| # |
| # Contributors: |
| # Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation |
| #=============================================================================*/ |
| |
| package org.eclipse.statet.internal.rhelp.core; |
| |
| import java.io.BufferedInputStream; |
| import java.io.BufferedOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.StandardCopyOption; |
| import java.nio.file.StandardOpenOption; |
| import java.util.List; |
| |
| import org.eclipse.statet.jcommons.collections.ImCollections; |
| import org.eclipse.statet.jcommons.collections.ImList; |
| import org.eclipse.statet.jcommons.lang.NonNull; |
| import org.eclipse.statet.jcommons.lang.NonNullByDefault; |
| import org.eclipse.statet.jcommons.lang.Nullable; |
| import org.eclipse.statet.jcommons.status.ErrorStatus; |
| |
| import org.eclipse.statet.rhelp.core.DocResource; |
| import org.eclipse.statet.rhelp.core.REnvHelpConfiguration; |
| import org.eclipse.statet.rhelp.core.RHelpCore; |
| import org.eclipse.statet.rhelp.core.RHelpKeyword; |
| import org.eclipse.statet.rhelp.core.RHelpKeywordGroup; |
| import org.eclipse.statet.rhelp.core.RHelpPage; |
| import org.eclipse.statet.rhelp.core.RPkgHelp; |
| import org.eclipse.statet.rj.renv.core.BasicRLibLocation; |
| import org.eclipse.statet.rj.renv.core.BasicRPkgDescription; |
| import org.eclipse.statet.rj.renv.core.REnvConfiguration; |
| import org.eclipse.statet.rj.renv.core.RLibLocation; |
| import org.eclipse.statet.rj.renv.core.RNumVersion; |
| import org.eclipse.statet.rj.renv.core.RPkgDescription; |
| |
| |
| @NonNullByDefault |
| public class SerUtil { |
| |
| // TODO in next ser version: replace urlString by url list |
| |
| public static final int VERSION_12= 12; |
| public static final int VERSION_11= 11; |
| public static final int VERSION_10= 10; |
| public static final int CURRENT_VERSION= VERSION_12; |
| |
| public static final int[] KNOWN_VERSIONS= { |
| VERSION_12, |
| VERSION_11, |
| VERSION_10 }; |
| |
| private static final int COMPRESS= 1 << 4; |
| |
| |
| private static final String RHELP_SER_FILE= "rhelp.ser"; //$NON-NLS-1$ |
| |
| |
| private static class UnsupportedVersionException extends IOException { |
| |
| private static final long serialVersionUID= 1L; |
| |
| public UnsupportedVersionException(final int inputVersion) { |
| super("input= " + inputVersion); |
| } |
| |
| } |
| |
| public static interface Controller { |
| |
| Object getFileLock(); |
| |
| boolean shouldSave(); |
| void onSaved(); |
| |
| } |
| |
| |
| public static @Nullable Path getIndexDirectoryPath(final REnvHelpConfiguration rEnvConfig) { |
| try { |
| final Path stateDirectory= rEnvConfig.getStateSharedDirectoryPath(); |
| if (stateDirectory != null) { |
| return stateDirectory.resolve("index"); //$NON-NLS-1$ |
| } |
| } |
| catch (final Exception e) {} |
| return null; |
| } |
| |
| public static Path getIndexDirectoryChecked(final REnvHelpConfiguration rEnvConfig) { |
| Exception e1= null; |
| try { |
| final Path stateDirectory= rEnvConfig.getStateSharedDirectoryPath(); |
| if (stateDirectory != null) { |
| final Path indexDirectory= stateDirectory.resolve("index"); //$NON-NLS-1$ |
| if (!Files.isDirectory(indexDirectory)) { |
| Files.createDirectories(indexDirectory); |
| } |
| return indexDirectory; |
| } |
| } |
| catch (final Exception e) { |
| e1= e; |
| } |
| throw new RuntimeException( |
| String.format("Index directory could not be resolved: '%1$s'.", |
| rEnvConfig.getStateSharedDirectory() ), |
| e1 ); |
| } |
| |
| public static Path getBasicDataFilePath(final Path indexDirectory) { |
| return indexDirectory.resolve(RHELP_SER_FILE); |
| } |
| |
| public static @Nullable Path getBasicDataFilePath(final REnvHelpConfiguration rEnvConfig) { |
| final Path directory= getIndexDirectoryPath(rEnvConfig); |
| if (directory != null) { |
| return getBasicDataFilePath(directory); |
| } |
| return null; |
| } |
| |
| |
| public SerUtil() { |
| } |
| |
| |
| public boolean canRead(final int version, final String type) { |
| switch (version) { |
| case VERSION_12: |
| case VERSION_11: |
| return true; |
| case VERSION_10: |
| return (type == REnvConfiguration.SHARED_SERVER); |
| default: |
| return false; |
| } |
| } |
| |
| |
| public boolean save(final REnvHelpConfiguration rEnvConfig, final REnvHelpImpl help, |
| final Controller controller) { |
| try { |
| final Path directory= getIndexDirectoryChecked(rEnvConfig); |
| final Path newFile= directory.resolve(RHELP_SER_FILE + ".new"); //$NON-NLS-1$ |
| |
| Files.deleteIfExists(newFile); |
| try (final DataStream out= DataStream.get(new BufferedOutputStream( |
| Files.newOutputStream(newFile, StandardOpenOption.CREATE_NEW) ))) { |
| save(help, out, COMPRESS); |
| out.flush(); |
| } |
| |
| synchronized (controller.getFileLock()) { |
| if (!controller.shouldSave()) { |
| return false; |
| } |
| final Path serFile= directory.resolve(RHELP_SER_FILE); |
| Files.move(newFile, serFile, StandardCopyOption.REPLACE_EXISTING); |
| controller.onSaved(); |
| return true; |
| } |
| } |
| catch (final Exception e) { |
| RHelpCoreInternals.log(new ErrorStatus(RHelpCore.BUNDLE_ID, |
| String.format("An error occurred when saving R help data for '%1$s'.", |
| rEnvConfig.getName() ), |
| e )); |
| return false; |
| } |
| } |
| |
| public boolean save(final REnvHelpConfiguration rEnvConfig, final InputStream in, |
| final Controller controller) { |
| try { |
| final Path directory= getIndexDirectoryChecked(rEnvConfig); |
| final Path newFile= directory.resolve(RHELP_SER_FILE + ".new"); //$NON-NLS-1$ |
| |
| Files.copy(in, newFile, StandardCopyOption.REPLACE_EXISTING); |
| |
| synchronized (controller.getFileLock()) { |
| if (!controller.shouldSave()) { |
| return false; |
| } |
| final Path serFile= directory.resolve(RHELP_SER_FILE); |
| Files.move(newFile, serFile, StandardCopyOption.REPLACE_EXISTING); |
| controller.onSaved(); |
| return true; |
| } |
| } |
| catch (final Exception e) { |
| RHelpCoreInternals.log(new ErrorStatus(RHelpCore.BUNDLE_ID, |
| String.format("An error occurred when saving R help data for '%1$s'.", |
| rEnvConfig.getName() ), |
| e )); |
| return false; |
| } |
| } |
| |
| public long getStamp(final REnvHelpConfiguration rEnvConfig) { |
| try { |
| final Path serFile= getBasicDataFilePath(rEnvConfig); |
| if (!Files.isRegularFile(serFile)) { |
| return REnvHelpImpl.NOT_AVAILABLE_STAMP; |
| } |
| |
| try (final DataStream in= DataStream.get(new BufferedInputStream( |
| Files.newInputStream(serFile), 64 ))) { |
| final int version= in.readVersion(); |
| if (version != CURRENT_VERSION |
| && !canRead(version, rEnvConfig.getStateSharedType())) { |
| return REnvHelpImpl.NOT_AVAILABLE_STAMP; |
| } |
| return in.readLong(); |
| } |
| } |
| catch (final Throwable e) { |
| if (e instanceof Error && !(e instanceof UnsupportedVersionException)) { |
| throw (Error) e; |
| } |
| RHelpCoreInternals.log(new ErrorStatus(RHelpCore.BUNDLE_ID, |
| String.format("An error occurred when loading R help data for '%1$s'.", |
| rEnvConfig.getName() ), |
| e )); |
| return REnvHelpImpl.NOT_AVAILABLE_STAMP; |
| } |
| } |
| |
| public @Nullable REnvHelpImpl load(final REnvHelpConfiguration rEnvConfig) { |
| try { |
| final Path serFile= getBasicDataFilePath(rEnvConfig); |
| if (!Files.isRegularFile(serFile)) { |
| return null; |
| } |
| |
| try (final DataStream in= DataStream.get(new BufferedInputStream( |
| Files.newInputStream(serFile) ))) { |
| return load(rEnvConfig, in); |
| } |
| } |
| catch (final Throwable e) { |
| if (e instanceof Error) { |
| throw (Error) e; |
| } |
| RHelpCoreInternals.log(new ErrorStatus(RHelpCore.BUNDLE_ID, |
| String.format("An error occurred when loading R help data for '%1$s'.", |
| rEnvConfig.getName() ), |
| e )); |
| return null; |
| } |
| } |
| |
| public void save(final REnvHelpImpl help, final DataStream out, final int flags) |
| throws IOException { |
| out.writeVersion(CURRENT_VERSION); |
| out.writeLong(help.getStamp()); |
| |
| out.writeInt(flags); |
| if ((flags & COMPRESS) != 0) { |
| out.enableCompression(); |
| } |
| |
| out.writeString(help.getDocDir()); |
| { final ImList<DocResource> resources= help.getManuals(); |
| final int count= resources.size(); |
| out.writeInt(count); |
| for (int i= 0; i < count; i++) { |
| saveDocResource(resources.get(i), out); |
| } |
| } |
| { final ImList<DocResource> resources= help.getMiscResources(); |
| final int count= resources.size(); |
| out.writeInt(count); |
| for (int i= 0; i < count; i++) { |
| saveDocResource(resources.get(i), out); |
| } |
| } |
| |
| { final List<RHelpKeywordGroup> keywordGroups= help.getKeywords(); |
| final int count= keywordGroups.size(); |
| out.writeInt(count); |
| for (int i= 0; i < keywordGroups.size(); i++) { |
| saveKeywordGroup(keywordGroups.get(i), out); |
| } |
| } |
| { final List<RPkgHelp> packages= help.getPkgs(); |
| final int count= packages.size(); |
| out.writeInt(count); |
| for (int i= 0; i < packages.size(); i++) { |
| savePackage(packages.get(i), out); |
| } |
| } |
| } |
| |
| public REnvHelpImpl load(final REnvHelpConfiguration rEnvConfig, final DataStream in) |
| throws IOException { |
| final int version= in.readVersion(); |
| if (version != CURRENT_VERSION |
| && !canRead(version, rEnvConfig.getStateSharedType())) { |
| throw new UnsupportedVersionException(version); |
| } |
| |
| final long stamp= in.readLong(); |
| |
| final int flags= in.readInt(); |
| if ((flags & COMPRESS) != 0) { |
| in.enableCompression(); |
| } |
| |
| final String docDir= in.readString(); |
| final ImList<DocResource> manuals; |
| { final int count= in.readInt(); |
| final DocResource[] array= new @NonNull DocResource[count]; |
| for (int i= 0; i < count; i++) { |
| array[i]= readDocResource(in); |
| } |
| manuals= ImCollections.newList(array); |
| } |
| final ImList<DocResource> miscRes; |
| { final int count= in.readInt(); |
| final DocResource[] array= new @NonNull DocResource[count]; |
| for (int i= 0; i < count; i++) { |
| array[i]= readDocResource(in); |
| } |
| miscRes= ImCollections.newList(array); |
| } |
| |
| final ImList<RHelpKeywordGroup> keywordGroups; |
| { final int count= in.readInt(); |
| final RHelpKeywordGroup[] array= new @NonNull RHelpKeywordGroup[count]; |
| for (int i= 0; i < count; i++) { |
| array[i]= loadKeywordGroup(in); |
| } |
| keywordGroups= ImCollections.newList(array); |
| } |
| |
| final ImList<RPkgHelp> pkgHelps; |
| { final int count= in.readInt(); |
| final RPkgHelp[] array= new @NonNull RPkgHelp[count]; |
| for (int i= 0; i < count; i++) { |
| array[i]= loadPackage(rEnvConfig, in); |
| } |
| pkgHelps= ImCollections.newList(array); |
| } |
| |
| return new REnvHelpImpl(rEnvConfig.getREnv(), version, stamp, |
| docDir, manuals, miscRes, |
| keywordGroups, pkgHelps ); |
| } |
| |
| |
| private void saveDocResource(final DocResource res, |
| final DataStream out) throws IOException { |
| out.writeString(res.getTitle()); |
| out.writeString(res.getPath()); |
| out.writeString(res.getPdfPath()); |
| } |
| |
| private DocResource readDocResource(final DataStream in) throws IOException { |
| return new DocResource( |
| in.readNonNullString(), |
| in.readNonNullString(), |
| in.readString() ); |
| } |
| |
| |
| private void saveKeywordGroup(final RHelpKeywordGroup group, |
| final DataStream out) throws IOException { |
| out.writeString(group.getLabel()); |
| out.writeString(group.getDescription()); |
| final List<RHelpKeyword> keywords= group.getNestedKeywords(); |
| final int count= keywords.size(); |
| out.writeInt(count); |
| for (int i= 0; i < count; i++) { |
| saveKeyword(keywords.get(i), out); |
| } |
| } |
| |
| private RHelpKeywordGroup loadKeywordGroup(final DataStream in) throws IOException { |
| final String label= in.readNonNullString(); |
| final String description= in.readNonNullString(); |
| final int count= in.readInt(); |
| final RHelpKeyword[] keywords= new @NonNull RHelpKeyword[count]; |
| for (int i= 0; i < count; i++) { |
| keywords[i]= loadKeyword(in); |
| } |
| return new RHelpKeywordGroupImpl(label, description, ImCollections.newList(keywords)); |
| } |
| |
| private void saveKeyword(final RHelpKeyword keyword, final DataStream out) |
| throws IOException { |
| out.writeString(keyword.getKeyword()); |
| out.writeString(keyword.getDescription()); |
| final List<RHelpKeyword> nestedKeywords= keyword.getNestedKeywords(); |
| final int count= nestedKeywords.size(); |
| out.writeInt(count); |
| for (int i= 0; i < nestedKeywords.size(); i++) { |
| saveKeyword(nestedKeywords.get(i), out); |
| } |
| } |
| |
| private RHelpKeyword loadKeyword(final DataStream in) |
| throws IOException { |
| final String keyword= in.readNonNullString(); |
| final String description= in.readNonNullString(); |
| final int n= in.readInt(); |
| final RHelpKeyword[] nestedKeywords= new @NonNull RHelpKeyword[n]; |
| for (int i= 0; i < n; i++) { |
| nestedKeywords[i]= loadKeyword(in); |
| } |
| return new RHelpKeywordImpl(keyword, description, ImCollections.newList(nestedKeywords)); |
| } |
| |
| private void savePackage(final RPkgHelp pkgHelp, final DataStream out) |
| throws IOException { |
| savePkgDescription(pkgHelp.getPkgDescription(), out); |
| |
| final List<RHelpPage> pages= pkgHelp.getPages(); |
| final int nPages= pages.size(); |
| out.writeInt(nPages); |
| for (int i= 0; i < nPages; i++) { |
| savePage(pages.get(i), out); |
| } |
| } |
| |
| private RPkgHelp loadPackage(final REnvHelpConfiguration rEnvConfig, final DataStream in) |
| throws IOException { |
| final RPkgDescription pkgDescription= loadPkgDescription(rEnvConfig, in); |
| |
| final int nPages= in.readInt(); |
| final RHelpPage[] pages= new @NonNull RHelpPage[nPages]; |
| final RPkgHelpImpl pkg= new RPkgHelpImpl(pkgDescription, rEnvConfig.getREnv()); |
| for (int i= 0; i < nPages; i++) { |
| pages[i]= loadPage(pkg, in); |
| } |
| pkg.setPages(ImCollections.newList(pages)); |
| return pkg; |
| } |
| |
| |
| private void savePkgDescription(final RPkgDescription pkgDescription, |
| final DataStream out) throws IOException { |
| out.writeString(pkgDescription.getName()); |
| out.writeString(pkgDescription.getVersion().toString()); |
| out.writeString(pkgDescription.getTitle()); |
| out.writeString(pkgDescription.getDescription()); |
| out.writeString(pkgDescription.getAuthor()); |
| out.writeString(pkgDescription.getMaintainer()); |
| out.writeString(joinUrls(pkgDescription.getUrls())); |
| out.writeString(pkgDescription.getBuilt()); |
| out.writeString(pkgDescription.getLibLocation().getDirectory()); |
| } |
| |
| private RPkgDescription loadPkgDescription(final REnvHelpConfiguration rEnvConfig, |
| final DataStream in) throws IOException { |
| final String name= in.readNonNullString().intern(); |
| final String version= in.readNonNullString(); |
| final String title= in.readNonNullString(); |
| final String description= in.readNonNullString(); |
| final String author= in.readString(); |
| final String maintainer= in.readString(); |
| final ImList<String> urls= splitUrls(in.readString()); |
| final String built= in.readNonNullString(); |
| final RLibLocation libLocation= getLibLocationSafe(rEnvConfig, in.readNonNullString()); |
| |
| return new BasicRPkgDescription(name, RNumVersion.create(version), |
| title, description, |
| author, maintainer, |
| urls, |
| built, libLocation); |
| } |
| |
| private @Nullable String joinUrls(final List<String> urls) { |
| if (urls.isEmpty()) { |
| return null; |
| } |
| return String.join(",", urls); //$NON-NLS-1$ |
| } |
| |
| private ImList<String> splitUrls(final @Nullable String urlString) { |
| if (urlString == null) { |
| return ImCollections.emptyList(); |
| } |
| return ImCollections.newList(urlString.split(",")); //$NON-NLS-1$ |
| } |
| |
| private RLibLocation getLibLocationSafe(final REnvHelpConfiguration rEnvConfig, final String directory) { |
| RLibLocation libLocation= rEnvConfig.getRLibLocationByDirectory(directory); |
| if (libLocation == null) { |
| libLocation= new BasicRLibLocation(RLibLocation.MISSING, directory, null); |
| } |
| return libLocation; |
| } |
| |
| |
| private void savePage(final RHelpPage page, final DataStream out) |
| throws IOException { |
| if (out.getVersion() >= VERSION_12) { |
| out.writeByte(RHelpPageImpl.createFlags(page.isInternal())); |
| } |
| out.writeString(page.getName()); |
| { final ImList<String> topics= page.getTopics(); |
| final int nTopics= topics.size(); |
| out.writeInt(nTopics); |
| for (int i= 0; i < nTopics; i++) { |
| final String topic= topics.get(i); |
| out.writeString((topic == page.getName()) ? null : topic); |
| } |
| } |
| out.writeString(page.getTitle()); |
| } |
| |
| private RHelpPage loadPage(final RPkgHelp pkg, final DataStream in) |
| throws IOException { |
| final byte pageFlags= (in.getVersion() >= VERSION_12) ? |
| in.readByte() : |
| 0; |
| final String name= in.readNonNullString().intern(); |
| final ImList<String> topics; |
| { final int nTopics= in.readInt(); |
| final String[] array= new @NonNull String[nTopics]; |
| for (int i= 0; i < array.length; i++) { |
| final String topic= in.readString(); |
| array[i]= (topic == null) ? name : topic.intern(); |
| } |
| topics= ImCollections.newList(array); |
| } |
| final String title= in.readNonNullString(); |
| return new RHelpPageImpl(pkg, name, pageFlags, topics, title); |
| } |
| |
| } |