Skip to content

Commit ce4942f

Browse files
tulinkryVladimir Kotal
authored andcommitted
fixing serialization bug with order of class properties (#1698)
1 parent d5940a5 commit ce4942f

File tree

4 files changed

+206
-13
lines changed

4 files changed

+206
-13
lines changed

src/org/opensolaris/opengrok/configuration/Group.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.TreeSet;
2828
import java.util.regex.Pattern;
2929
import java.util.regex.PatternSyntaxException;
30+
import org.opensolaris.opengrok.util.ClassUtil;
3031

3132
/**
3233
* Placeholder for the information about subgroups of projects and repositories.
@@ -38,6 +39,10 @@
3839
*/
3940
public class Group implements Comparable<Group>, Nameable {
4041

42+
static {
43+
ClassUtil.remarkTransientFields(Group.class);
44+
}
45+
4146
private String name;
4247
/**
4348
* Group regexp pattern.
@@ -60,13 +65,13 @@ public class Group implements Comparable<Group>, Nameable {
6065
*/
6166
private Pattern compiledPattern = Pattern.compile("()");
6267
private Group parent;
63-
private int flag;
64-
6568
private Set<Group> subgroups = new TreeSet<>();
66-
private Set<Group> descendants = new TreeSet<>();
67-
private Set<Project> projects = new TreeSet<>();
68-
private Set<Project> repositories = new TreeSet<>();
69-
private Set<Group> parents;
69+
70+
private transient int flag;
71+
private transient Set<Group> descendants = new TreeSet<>();
72+
private transient Set<Project> projects = new TreeSet<>();
73+
private transient Set<Project> repositories = new TreeSet<>();
74+
private transient Set<Group> parents;
7075

7176
/**
7277
* No-arg constructor is needed for deserialization.

src/org/opensolaris/opengrok/configuration/Project.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.util.Locale;
3030
import java.util.Set;
3131
import java.util.TreeSet;
32+
import org.opensolaris.opengrok.util.ClassUtil;
3233

3334
/**
3435
* Placeholder for the information that builds up a project
@@ -37,6 +38,10 @@ public class Project implements Comparable<Project>, Nameable, Serializable {
3738

3839
private static final long serialVersionUID = 1L;
3940

41+
static {
42+
ClassUtil.remarkTransientFields(Project.class);
43+
}
44+
4045
private String path;
4146

4247
/**
@@ -67,17 +72,17 @@ public class Project implements Comparable<Project>, Nameable, Serializable {
6772
/**
6873
* Set of groups which match this project.
6974
*/
70-
private Set<Group> groups = new TreeSet<>();
75+
private transient Set<Group> groups = new TreeSet<>();
7176

7277
// This empty constructor is needed for serialization.
73-
public Project () {
78+
public Project() {
7479
}
7580

76-
public Project (String name) {
81+
public Project(String name) {
7782
this.name = name;
7883
}
7984

80-
public Project (String name, String path) {
85+
public Project(String name, String path) {
8186
this.name = name;
8287
this.path = path;
8388
}
@@ -128,8 +133,8 @@ public int getTabSize() {
128133
* Set a textual name of this project, preferably don't use " , " in the
129134
* name, since it's used as delimiter for more projects
130135
*
131-
* XXX we should not allow setting project name after it has been constructed
132-
* because it is probably part of HashMap.
136+
* XXX we should not allow setting project name after it has been
137+
* constructed because it is probably part of HashMap.
133138
*
134139
* @param name a textual name of the project
135140
*/
@@ -278,7 +283,7 @@ public static Project getByName(String name) {
278283
if (env.hasProjects()) {
279284
Project proj;
280285
if ((proj = env.getProjects().get(name)) != null) {
281-
return (proj);
286+
return (proj);
282287
}
283288
}
284289
return null;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* CDDL HEADER START
3+
*
4+
* The contents of this file are subject to the terms of the
5+
* Common Development and Distribution License (the "License").
6+
* You may not use this file except in compliance with the License.
7+
*
8+
* See LICENSE.txt included in this distribution for the specific
9+
* language governing permissions and limitations under the License.
10+
*
11+
* When distributing Covered Code, include this CDDL HEADER in each
12+
* file and include the License file at LICENSE.txt.
13+
* If applicable, add the following below this CDDL HEADER, with the
14+
* fields enclosed by brackets "[]" replaced with your own identifying
15+
* information: Portions Copyright [yyyy] [name of copyright owner]
16+
*
17+
* CDDL HEADER END
18+
*/
19+
20+
/*
21+
* Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
22+
*/
23+
package org.opensolaris.opengrok.util;
24+
25+
import java.beans.BeanInfo;
26+
import java.beans.IntrospectionException;
27+
import java.beans.Introspector;
28+
import java.beans.PropertyDescriptor;
29+
import java.lang.reflect.Field;
30+
import java.lang.reflect.Modifier;
31+
import java.util.logging.Level;
32+
import java.util.logging.Logger;
33+
import org.opensolaris.opengrok.configuration.Project;
34+
import org.opensolaris.opengrok.logger.LoggerFactory;
35+
36+
/**
37+
*
38+
* @author Krystof Tulinger
39+
*/
40+
public class ClassUtil {
41+
42+
private static final Logger LOGGER = LoggerFactory.getLogger(ClassUtil.class);
43+
44+
/**
45+
* Mark all transient fields in {@code targetClass} as @Transient for the
46+
* XML serialization.
47+
*
48+
* Fields marked with java transient keyword do not work becase the
49+
* XMLEncoder does not take these into account. This helper marks the fields
50+
* marked with transient keyword as transient also for the XMLDecoder.
51+
*
52+
* @param targetClass the class
53+
*/
54+
public static void remarkTransientFields(Class targetClass) {
55+
try {
56+
BeanInfo info;
57+
info = Introspector.getBeanInfo(targetClass);
58+
PropertyDescriptor[] propertyDescriptors = info.getPropertyDescriptors();
59+
for (Field f : Project.class.getDeclaredFields()) {
60+
if (Modifier.isTransient(f.getModifiers())) {
61+
for (int i = 0; i < propertyDescriptors.length; ++i) {
62+
if (propertyDescriptors[i].getName().equals(f.getName())) {
63+
propertyDescriptors[i].setValue("transient", Boolean.TRUE);
64+
}
65+
}
66+
}
67+
}
68+
} catch (IntrospectionException ex) {
69+
LOGGER.log(Level.WARNING, "An exception ocurred during remarking transient fields:", ex);
70+
}
71+
}
72+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* CDDL HEADER START
3+
*
4+
* The contents of this file are subject to the terms of the
5+
* Common Development and Distribution License (the "License").
6+
* You may not use this file except in compliance with the License.
7+
*
8+
* See LICENSE.txt included in this distribution for the specific
9+
* language governing permissions and limitations under the License.
10+
*
11+
* When distributing Covered Code, include this CDDL HEADER in each
12+
* file and include the License file at LICENSE.txt.
13+
* If applicable, add the following below this CDDL HEADER, with the
14+
* fields enclosed by brackets "[]" replaced with your own identifying
15+
* information: Portions Copyright [yyyy] [name of copyright owner]
16+
*
17+
* CDDL HEADER END
18+
*/
19+
20+
/*
21+
* Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
22+
*/
23+
package org.opensolaris.opengrok.configuration;
24+
25+
import java.beans.XMLDecoder;
26+
import java.beans.XMLEncoder;
27+
import java.io.ByteArrayInputStream;
28+
import java.io.ByteArrayOutputStream;
29+
import java.io.IOException;
30+
import java.util.LinkedList;
31+
import junit.framework.AssertionFailedError;
32+
import org.junit.Test;
33+
import org.opensolaris.opengrok.util.ClassUtil;
34+
35+
import static org.junit.Assert.assertEquals;
36+
import static org.junit.Assert.assertNotNull;
37+
38+
/**
39+
*
40+
* @author Krystof Tulinger
41+
*/
42+
public class ConfigurationTest {
43+
44+
/**
45+
* Test for a serialization bug in configuration. The problem is that with
46+
* this scenario the output is written in a way that when deserializing the
47+
* input later on, we get {@link NullPointerException} trying to use
48+
* {@link Group#compareTo(Group)}. This exception is caused by wrong order
49+
* of serialization of
50+
* {@link Group#getDescendants()}, {@link Group#getParent()} and
51+
* {@link Project#getGroups()} where the backpointers in a {@link Project}
52+
* to several {@link Group}s shall be stored in a set while this
53+
* {@link Group} does not have a name yet (= {@code null}).
54+
*
55+
* @throws IOException
56+
* @see ClassUtil#remarkTransientFields(java.lang.Class)
57+
* ClassUtil#remarkTransientFields() for suggested solution
58+
*/
59+
@Test
60+
public void serializationOrderTest() throws IOException {
61+
Project project = new Project("project");
62+
Group apache = new Group("Apache", "test.*");
63+
Group bsd = new Group("BSD", "test.*");
64+
Group opensource = new Group("OpenSource", "test.*");
65+
66+
opensource.addGroup(apache);
67+
opensource.addGroup(bsd);
68+
69+
bsd.addProject(project);
70+
opensource.addProject(project);
71+
72+
project.getGroups().add(opensource);
73+
project.getGroups().add(bsd);
74+
75+
Configuration cfg = new Configuration();
76+
Configuration oldCfg = cfg;
77+
cfg.addGroup(apache);
78+
cfg.addGroup(bsd);
79+
cfg.addGroup(opensource);
80+
81+
ByteArrayOutputStream out = new ByteArrayOutputStream();
82+
83+
try (XMLEncoder enc = new XMLEncoder(out)) {
84+
enc.writeObject(cfg);
85+
}
86+
87+
// Create an exception listener to detect errors while encoding and
88+
// decoding
89+
final LinkedList<Exception> exceptions = new LinkedList<>();
90+
91+
try (ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
92+
XMLDecoder dec = new XMLDecoder(in, null, (Exception e) -> {
93+
exceptions.addLast(e);
94+
})) {
95+
96+
cfg = (Configuration) dec.readObject();
97+
assertNotNull(cfg);
98+
99+
// verify that the read didn't fail
100+
if (!exceptions.isEmpty()) {
101+
AssertionFailedError afe = new AssertionFailedError(
102+
"Got " + exceptions.size() + " exception(s)");
103+
// Can only chain one of the exceptions. Take the first one.
104+
afe.initCause(exceptions.getFirst());
105+
throw afe;
106+
}
107+
108+
assertEquals(oldCfg.getGroups(), cfg.getGroups());
109+
}
110+
}
111+
}

0 commit comments

Comments
 (0)