001 package org.cumulus4j.store.datastoreversion;
002
003 import java.io.StringReader;
004 import java.io.StringWriter;
005 import java.util.ArrayList;
006 import java.util.Collections;
007 import java.util.Date;
008 import java.util.HashMap;
009 import java.util.HashSet;
010 import java.util.List;
011 import java.util.Locale;
012 import java.util.Map;
013 import java.util.Properties;
014 import java.util.Set;
015 import java.util.concurrent.atomic.AtomicBoolean;
016
017 import javax.jdo.FetchPlan;
018 import javax.jdo.PersistenceManager;
019
020 import org.cumulus4j.store.Cumulus4jStoreManager;
021 import org.cumulus4j.store.WorkInProgressException;
022 import org.cumulus4j.store.crypto.CryptoContext;
023 import org.cumulus4j.store.datastoreversion.command.IntroduceKeyStoreRefID;
024 import org.cumulus4j.store.datastoreversion.command.MigrateToSequence2;
025 import org.cumulus4j.store.datastoreversion.command.MinimumCumulus4jVersion;
026 import org.cumulus4j.store.datastoreversion.command.RecreateIndex;
027 import org.cumulus4j.store.model.DatastoreVersion;
028 import org.cumulus4j.store.model.DatastoreVersionDAO;
029 import org.cumulus4j.store.model.KeyStoreRef;
030
031 /**
032 * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
033 */
034 @SuppressWarnings("unchecked")
035 public class DatastoreVersionManager {
036
037 public static final int MANAGER_VERSION = 1;
038
039 private static final Class<?>[] datastoreVersionCommandClasses = {
040 // MinimumCumulus4jVersion should be the very first entry!
041 MinimumCumulus4jVersion.class,
042
043 IntroduceKeyStoreRefID.class,
044 MigrateToSequence2.class,
045 RecreateIndex.class
046 };
047
048 private static final List<Class<? extends DatastoreVersionCommand>> datastoreVersionCommandClassList;
049 static {
050 List<Class<? extends DatastoreVersionCommand>> list = new ArrayList<Class<? extends DatastoreVersionCommand>>(datastoreVersionCommandClasses.length);
051 for (Class<?> c : datastoreVersionCommandClasses) {
052 if (c == null)
053 throw new IllegalStateException("datastoreVersionCommandClasses contains null element!");
054
055 if (!DatastoreVersionCommand.class.isAssignableFrom(c))
056 throw new IllegalStateException(String.format("%s does not implement %s!", c.getName(), DatastoreVersionCommand.class.getName()));
057
058 list.add((Class<? extends DatastoreVersionCommand>) c);
059 }
060 datastoreVersionCommandClassList = Collections.unmodifiableList(list);
061 }
062
063 private Cumulus4jStoreManager storeManager;
064 private Set<Integer> performedKeyStoreRefIDs = Collections.synchronizedSet(new HashSet<Integer>());
065 private AtomicBoolean performedGlobally = new AtomicBoolean();
066
067 public DatastoreVersionManager(Cumulus4jStoreManager storeManager) {
068 if (storeManager == null)
069 throw new IllegalArgumentException("storeManager == null");
070
071 this.storeManager = storeManager;
072 }
073
074 public synchronized void applyOnce(CryptoContext cryptoContext) {
075 final Integer keyStoreRefID = cryptoContext.getKeyStoreRefID();
076
077 // We do not need synchronisation here, because the 'performedKeyStoreRefIDs' is a synchronized set
078 // and only one single thread will succeed in adding the keyStoreRefID.
079 // WRONG! We do need synchronisation, because we must ensure that there is no access to the datastore,
080 // before it has been converted to the newest version. Hence this method is 'synchronized'.
081 // It's only the question how we can do this in a cluster-environment. But that does not matter, right now.
082 // We might later add some DB-based lock.
083 // Marco :-)
084 boolean error1 = true;
085 try {
086 // Immediately set 'performed' to prevent endless recursions. Remove again in case of exception!
087 if (performedKeyStoreRefIDs.add(keyStoreRefID)) {
088
089 // Again no need for synchronisation because of AtomicBoolean 'performedGlobally'.
090 boolean error2 = true;
091 try {
092 if (performedGlobally.compareAndSet(false, true)) {
093 apply(cryptoContext, KeyStoreRef.GLOBAL_KEY_STORE_REF_ID);
094 }
095 error2 = false;
096 } finally {
097 if (error2)
098 performedGlobally.set(false);
099 }
100
101 apply(cryptoContext, keyStoreRefID);
102 }
103
104 error1 = false;
105 } finally {
106 if (error1)
107 performedKeyStoreRefIDs.remove(keyStoreRefID);
108 }
109 }
110
111 protected void apply(CryptoContext cryptoContext, int keyStoreRefID) {
112
113 List<PersistenceManager> persistenceManagers = new ArrayList<PersistenceManager>(2);
114 persistenceManagers.add(cryptoContext.getPersistenceManagerForData());
115 if (cryptoContext.getPersistenceManagerForData() != cryptoContext.getPersistenceManagerForIndex())
116 persistenceManagers.add(cryptoContext.getPersistenceManagerForIndex());
117
118 for (PersistenceManager pm : persistenceManagers) {
119 List<DatastoreVersionCommand> datastoreVersionCommands = createDatastoreVersionCommands();
120
121 DatastoreVersionDAO datastoreVersionDAO = new DatastoreVersionDAO(pm);
122 Map<String, DatastoreVersion> datastoreVersionID2DatastoreVersionMap = check(
123 cryptoContext, keyStoreRefID, pm, datastoreVersionDAO, datastoreVersionCommands
124 );
125 for (DatastoreVersionCommand datastoreVersionCommand : datastoreVersionCommands) {
126 if (KeyStoreRef.GLOBAL_KEY_STORE_REF_ID == keyStoreRefID && datastoreVersionCommand.isKeyStoreDependent())
127 continue;
128
129 if (KeyStoreRef.GLOBAL_KEY_STORE_REF_ID != keyStoreRefID && !datastoreVersionCommand.isKeyStoreDependent())
130 continue;
131
132 if (!isDatastoreVersionCommandEnabled(cryptoContext, datastoreVersionCommand))
133 continue;
134
135 try {
136 applyOneCommand(cryptoContext, keyStoreRefID, pm, datastoreVersionDAO, datastoreVersionID2DatastoreVersionMap, datastoreVersionCommand);
137 } catch (WorkInProgressException x) {
138 throw x;
139 } catch (Exception x) {
140 throw new CommandApplyException(
141 String.format("Applying command failed: commandID='%s': %s", datastoreVersionCommand.getCommandID(), x.toString()),
142 x
143 );
144 }
145 }
146 }
147 }
148
149 protected boolean isDatastoreVersionCommandEnabled(CryptoContext cryptoContext, DatastoreVersionCommand datastoreVersionCommand) {
150 String propertyKey = String.format("cumulus4j.DatastoreVersionCommand[%s].enabled", datastoreVersionCommand.getCommandID());
151 Object propertyValue = cryptoContext.getExecutionContext().getStoreManager().getProperty(propertyKey);
152 return propertyValue == null || !Boolean.FALSE.toString().toLowerCase(Locale.UK).equals(propertyValue.toString().toLowerCase(Locale.UK));
153 }
154
155 protected List<DatastoreVersionCommand> createDatastoreVersionCommands() {
156 List<DatastoreVersionCommand> datastoreVersionCommands = new ArrayList<DatastoreVersionCommand>(datastoreVersionCommandClassList.size());
157 try {
158 for (Class<? extends DatastoreVersionCommand> klass : datastoreVersionCommandClassList) {
159 DatastoreVersionCommand command = klass.newInstance();
160 datastoreVersionCommands.add(command);
161 }
162 } catch (InstantiationException e) {
163 throw new RuntimeException(e);
164 } catch (IllegalAccessException e) {
165 throw new RuntimeException(e);
166 }
167 return datastoreVersionCommands;
168 }
169
170 protected Map<String, DatastoreVersion> check(CryptoContext cryptoContext, int keyStoreRefID, PersistenceManager pm, DatastoreVersionDAO datastoreVersionDAO, List<DatastoreVersionCommand> datastoreVersionCommands) {
171 Map<String, DatastoreVersion> datastoreVersionID2DatastoreVersionMap = datastoreVersionDAO.getCommandID2DatastoreVersionMap(keyStoreRefID);
172
173 for (DatastoreVersionCommand datastoreVersionCommand : datastoreVersionCommands) {
174 DatastoreVersion datastoreVersion = datastoreVersionID2DatastoreVersionMap.get(datastoreVersionCommand.getCommandID());
175 if (datastoreVersionCommand.isFinal()) {
176 if (datastoreVersion != null && datastoreVersion.getCommandVersion() != datastoreVersionCommand.getCommandVersion()) {
177 throw new IllegalStateException(String.format(
178 "Final command class version does not match persistent version! datastoreVersionID='%s' datastoreVersionCommand.class='%s' datastoreVersionCommand.commandVersion=%s persistentDatastoreVersion.commandVersion=%s",
179 datastoreVersionCommand.getCommandID(),
180 datastoreVersionCommand.getClass().getName(),
181 datastoreVersionCommand.getCommandVersion(),
182 datastoreVersion.getCommandVersion()
183 ));
184 }
185 }
186 else if (datastoreVersion != null && datastoreVersion.getCommandVersion() > datastoreVersionCommand.getCommandVersion()) {
187 throw new IllegalStateException(String.format(
188 "Non-final command class version is lower than persistent version! Downgrading is not supported! datastoreVersionID='%s' datastoreVersionCommand.class='%s' datastoreVersionCommand.commandVersion=%s persistentDatastoreVersion.commandVersion=%s",
189 datastoreVersionCommand.getCommandID(),
190 datastoreVersionCommand.getClass().getName(),
191 datastoreVersionCommand.getCommandVersion(),
192 datastoreVersion.getCommandVersion()
193 ));
194 }
195 }
196
197 return datastoreVersionID2DatastoreVersionMap;
198 }
199
200 protected void applyOneCommand(
201 CryptoContext cryptoContext, int keyStoreRefID, PersistenceManager pm,
202 DatastoreVersionDAO datastoreVersionDAO, Map<String, DatastoreVersion> datastoreVersionID2DatastoreVersionMap, DatastoreVersionCommand datastoreVersionCommand
203 ) throws Exception
204 {
205 String datastoreVersionID = datastoreVersionCommand.getCommandID();
206 DatastoreVersion datastoreVersion = datastoreVersionID2DatastoreVersionMap.get(datastoreVersionID);
207 if (datastoreVersion == null ||
208 (
209 !datastoreVersionCommand.isFinal() &&
210 datastoreVersionCommand.getCommandVersion() != datastoreVersion.getCommandVersion()
211 )
212 )
213 {
214 DatastoreVersion datastoreVersionCopy = detachDatastoreVersion(pm, datastoreVersion);
215 // Map<String, DatastoreVersion> datastoreVersionID2DatastoreVersionMapCopy = detachDatastoreVersionID2DatastoreVersionMap(cryptoContext, pm, datastoreVersionID2DatastoreVersionMap);
216
217 Properties workInProgressStateProperties = new Properties();
218 if (datastoreVersion == null)
219 datastoreVersion = new DatastoreVersion(datastoreVersionID, keyStoreRefID);
220 else {
221 if (datastoreVersion.getWorkInProgressStateProperties() != null)
222 workInProgressStateProperties.load(new StringReader(datastoreVersion.getWorkInProgressStateProperties()));
223 }
224
225 // apply
226 try {
227 datastoreVersionCommand.apply(new CommandApplyParam(
228 storeManager, cryptoContext, pm, datastoreVersionCopy, workInProgressStateProperties
229 ));
230 } catch (WorkInProgressException x) {
231 datastoreVersion.setApplyTimestamp(new Date());
232 datastoreVersion.setWorkInProgressCommandVersion(datastoreVersionCommand.getCommandVersion());
233 datastoreVersion.setWorkInProgressManagerVersion(MANAGER_VERSION);
234 StringWriter writer = new StringWriter();
235 workInProgressStateProperties.store(writer, null);
236 datastoreVersion.setWorkInProgressStateProperties(writer.toString());
237 pm.flush();
238 throw x;
239 }
240
241 datastoreVersion.setApplyTimestamp(new Date());
242 datastoreVersion.setCommandVersion(datastoreVersionCommand.getCommandVersion());
243 datastoreVersion.setManagerVersion(MANAGER_VERSION);
244 datastoreVersion.setWorkInProgressCommandVersion(null);
245 datastoreVersion.setWorkInProgressManagerVersion(null);
246 datastoreVersion.setWorkInProgressStateProperties(""); // field does not accept null (no need for this extra info in the DB)
247 pm.makePersistent(datastoreVersion); // just in case, it's new - otherwise doesn't hurt
248 pm.flush(); // provoke early failure
249 }
250 }
251
252 protected DatastoreVersion detachDatastoreVersion(PersistenceManager pm, DatastoreVersion attached) {
253 pm.getFetchPlan().setGroup(FetchPlan.ALL);
254 pm.getFetchPlan().setMaxFetchDepth(-1);
255 return attached == null ? null : pm.detachCopy(attached);
256 }
257
258 protected Map<String, DatastoreVersion> detachDatastoreVersionID2DatastoreVersionMap(CryptoContext cryptoContext, PersistenceManager pm, Map<String, DatastoreVersion> datastoreVersionID2DatastoreVersionMap) {
259 Map<String, DatastoreVersion> result = new HashMap<String, DatastoreVersion>(datastoreVersionID2DatastoreVersionMap.size());
260 for (Map.Entry<String, DatastoreVersion> me : datastoreVersionID2DatastoreVersionMap.entrySet()) {
261 result.put(me.getKey(), detachDatastoreVersion(pm, me.getValue()));
262 }
263 return Collections.unmodifiableMap(result);
264 }
265 }