1 /*
2  * Copyright 2008-2019 by Emeric Vernat
3  *
4  *     This file is part of Java Melody.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */

18 package net.bull.javamelody.internal.model;
19
20 import java.io.BufferedInputStream;
21 import java.io.BufferedOutputStream;
22 import java.io.File;
23 import java.io.FileInputStream;
24 import java.io.FileOutputStream;
25 import java.io.FilenameFilter;
26 import java.io.IOException;
27 import java.io.ObjectInputStream;
28 import java.io.ObjectOutputStream;
29 import java.io.OutputStream;
30 import java.util.Arrays;
31 import java.util.Calendar;
32 import java.util.Collections;
33 import java.util.List;
34 import java.util.zip.GZIPInputStream;
35 import java.util.zip.GZIPOutputStream;
36
37 import net.bull.javamelody.Parameter;
38 import net.bull.javamelody.internal.common.Parameters;
39
40 /**
41  * Classe chargée de l'enregistrement et de la lecture d'un {@link Counter}.
42  * @author Emeric Vernat
43  */

44 public class CounterStorage {
45     private static final int DEFAULT_OBSOLETE_STATS_DAYS = 365;
46     private static boolean storageDisabled;
47     private final Counter counter;
48
49     // do not user CounterResponseStream to not depend on the net.bull.internal.web package
50     private static class CounterOutputStream extends OutputStream {
51         int dataLength;
52         private final OutputStream output;
53
54         CounterOutputStream(OutputStream output) {
55             super();
56             this.output = output;
57         }
58
59         @Override
60         public void write(int b) throws IOException {
61             output.write(b);
62             dataLength++;
63         }
64
65         @Override
66         public void write(byte[] b) throws IOException {
67             output.write(b);
68             dataLength += b.length;
69         }
70
71         @Override
72         public void write(byte[] b, int off, int len) throws IOException {
73             output.write(b, off, len);
74             dataLength += len;
75         }
76
77         @Override
78         public void flush() throws IOException {
79             output.flush();
80         }
81
82         @Override
83         public void close() throws IOException {
84             output.close();
85         }
86     }
87
88     /**
89      * Constructeur.
90      * @param counter Counter
91      */

92     CounterStorage(Counter counter) {
93         super();
94         assert counter != null;
95         this.counter = counter;
96     }
97
98     /**
99      * Enregistre le counter.
100      * @return Taille sérialisée non compressée du counter (estimation pessimiste de l'occupation mémoire)
101      * @throws IOException Exception d'entrée/sortie
102      */

103     int writeToFile() throws IOException {
104         if (storageDisabled) {
105             return -1;
106         }
107         final File file = getFile();
108         if (counter.getRequestsCount() == 0 && counter.getErrorsCount() == 0 && !file.exists()) {
109             // s'il n'y a pas de requête, inutile d'écrire des fichiers de compteurs vides
110             // (par exemple pour le compteur ejb s'il n'y a pas d'ejb)
111             return -1;
112         }
113         final File directory = file.getParentFile();
114         if (!directory.mkdirs() && !directory.exists()) {
115             throw new IOException("JavaMelody directory can't be created: " + directory.getPath());
116         }
117         return writeToFile(counter, file);
118     }
119
120     static int writeToFile(Counter counter, File file) throws IOException {
121         try (FileOutputStream out = new FileOutputStream(file)) {
122             final CounterOutputStream counterOutput = new CounterOutputStream(
123                     new GZIPOutputStream(new BufferedOutputStream(out)));
124             try (ObjectOutputStream output = new ObjectOutputStream(counterOutput)) {
125                 output.writeObject(counter);
126                 // ce close libère les ressources du ObjectOutputStream et du GZIPOutputStream
127             }
128             // retourne la taille sérialisée non compressée,
129             // qui est une estimation pessimiste de l'occupation mémoire
130             return counterOutput.dataLength;
131         }
132     }
133
134     /**
135      * Lecture du counter depuis son fichier et retour du résultat.
136      * @return Counter
137      * @throws IOException e
138      */

139     Counter readFromFile() throws IOException {
140         if (storageDisabled) {
141             return null;
142         }
143         final File file = getFile();
144         if (file.exists()) {
145             return readFromFile(file);
146         }
147         // ou on retourne null si le fichier n'existe pas
148         return null;
149     }
150
151     static Counter readFromFile(File file) throws IOException {
152         try (FileInputStream in = new FileInputStream(file)) {
153             try (ObjectInputStream input = TransportFormat
154                     .createObjectInputStream(new GZIPInputStream(new BufferedInputStream(in)))) {
155                 // on retourne l'instance du counter lue
156                 return (Counter) input.readObject();
157                 // ce close libère les ressources du ObjectInputStream et du GZIPInputStream
158             }
159         } catch (final ClassNotFoundException e) {
160             throw new IOException(e.getMessage(), e);
161         }
162     }
163
164     private File getFile() {
165         final File storageDirectory = Parameters.getStorageDirectory(counter.getApplication());
166         return new File(storageDirectory, counter.getStorageName() + ".ser.gz");
167     }
168
169     static long deleteObsoleteCounterFiles(String application) {
170         final Calendar nowMinusOneYearAndADay = Calendar.getInstance();
171         nowMinusOneYearAndADay.add(Calendar.DAY_OF_YEAR, -getObsoleteStatsDays());
172         nowMinusOneYearAndADay.add(Calendar.DAY_OF_YEAR, -1);
173         // filtre pour ne garder que les fichiers d'extension .ser.gz et pour éviter d'instancier des File inutiles
174         long diskUsage = 0;
175         for (final File file : listSerGzFiles(application)) {
176             boolean deleted = false;
177             if (file.lastModified() < nowMinusOneYearAndADay.getTimeInMillis()) {
178                 deleted = file.delete();
179             }
180             if (!deleted) {
181                 diskUsage += file.length();
182             }
183         }
184
185         // on retourne true si tous les fichiers .ser.gz obsolètes ont été supprimés, false sinon
186         return diskUsage;
187     }
188
189     /**
190      * @return Nombre de jours avant qu'un fichier de statistiques (extension .ser.gz),
191      * soit considéré comme obsolète et soit supprimé automatiquement, à minuit (365 par défaut, soit 1 an)
192      */

193     private static int getObsoleteStatsDays() {
194         final String param = Parameter.OBSOLETE_STATS_DAYS.getValue();
195         if (param != null) {
196             // lance une NumberFormatException si ce n'est pas un nombre
197             final int result = Integer.parseInt(param);
198             if (result <= 0) {
199                 throw new IllegalStateException(
200                         "The parameter obsolete-stats-days should be > 0 (365 recommended)");
201             }
202             return result;
203         }
204         return DEFAULT_OBSOLETE_STATS_DAYS;
205     }
206
207     private static List<File> listSerGzFiles(String application) {
208         final File storageDir = Parameters.getStorageDirectory(application);
209         // filtre pour ne garder que les fichiers d'extension .rrd et pour éviter d'instancier des File inutiles
210         final FilenameFilter filenameFilter = new FilenameFilter() {
211             /** {@inheritDoc} */
212             @Override
213             public boolean accept(File dir, String fileName) {
214                 return fileName.endsWith(".ser.gz");
215             }
216         };
217         final File[] files = storageDir.listFiles(filenameFilter);
218         if (files == null) {
219             return Collections.emptyList();
220         }
221         return Arrays.asList(files);
222     }
223
224     // cette méthode est utilisée dans l'ihm Swing
225     public static void disableStorage() {
226         storageDisabled = true;
227     }
228 }
229